From d2c4040dd6b1dccb50422cd82d4bc8fe245a3774 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:19:23 +0000 Subject: [PATCH] fix: properly complete Future when async task execution fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes GitHub issue #4072 where an async task that errors would keep its thread alive because the Future was never completed. The issue was in the _execute_task_async method which didn't handle exceptions from _execute_core. When an exception was raised, the future.set_result() was never called, leaving the Future in an incomplete state. This caused future.result() to block forever. The fix wraps the _execute_core call in a try-except block and calls future.set_exception(e) when an exception occurs, ensuring the Future is always properly completed. Added tests: - test_execute_async_basic: Basic threaded async execution - test_execute_async_exception_completes_future: Regression test for #4072 - test_execute_async_exception_sets_end_time: Verify end_time is set on error - test_execute_async_exception_does_not_hang: Verify no hang on error Co-Authored-By: João --- lib/crewai/src/crewai/task.py | 6 +- lib/crewai/tests/task/test_async_task.py | 92 +++++++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/lib/crewai/src/crewai/task.py b/lib/crewai/src/crewai/task.py index 85e8dbb17..e21ae46aa 100644 --- a/lib/crewai/src/crewai/task.py +++ b/lib/crewai/src/crewai/task.py @@ -494,7 +494,11 @@ class Task(BaseModel): future: Future[TaskOutput], ) -> None: """Execute the task asynchronously with context handling.""" - result = self._execute_core(agent, context, tools) + try: + result = self._execute_core(agent, context, tools) + except Exception as e: + future.set_exception(e) + return future.set_result(result) async def aexecute_sync( diff --git a/lib/crewai/tests/task/test_async_task.py b/lib/crewai/tests/task/test_async_task.py index 70fec377d..f6e4bb0fe 100644 --- a/lib/crewai/tests/task/test_async_task.py +++ b/lib/crewai/tests/task/test_async_task.py @@ -383,4 +383,94 @@ class TestAsyncTaskOutput: assert result.description == "Test description" assert result.expected_output == "Test expected" assert result.raw == "Test result" - assert result.agent == "Test Agent" \ No newline at end of file + assert result.agent == "Test Agent" + + +class TestThreadedAsyncExecution: + """Tests for threaded async task execution (execute_async with Future).""" + + @patch("crewai.Agent.execute_task") + def test_execute_async_basic( + self, mock_execute: MagicMock, test_agent: Agent + ) -> None: + """Test basic threaded async task execution.""" + mock_execute.return_value = "Async task result" + task = Task( + description="Test task description", + expected_output="Test expected output", + agent=test_agent, + ) + + future = task.execute_async() + result = future.result(timeout=5) + + assert result is not None + assert isinstance(result, TaskOutput) + assert result.raw == "Async task result" + assert result.agent == "Test Agent" + mock_execute.assert_called_once() + + @patch("crewai.Agent.execute_task") + def test_execute_async_exception_completes_future( + self, mock_execute: MagicMock, test_agent: Agent + ) -> None: + """Test that execute_async properly completes the Future when an exception occurs. + + This is a regression test for GitHub issue #4072 where an async task that + errors would keep its thread alive because the Future was never completed. + """ + mock_execute.side_effect = ValueError("Something happened here") + task = Task( + description="Test task description", + expected_output="Test expected output", + agent=test_agent, + ) + + future = task.execute_async() + + with pytest.raises(ValueError) as exc_info: + future.result(timeout=5) + + assert "Something happened here" in str(exc_info.value) + + @patch("crewai.Agent.execute_task") + def test_execute_async_exception_sets_end_time( + self, mock_execute: MagicMock, test_agent: Agent + ) -> None: + """Test that execute_async sets end_time even when an exception occurs.""" + mock_execute.side_effect = RuntimeError("Test error") + task = Task( + description="Test task description", + expected_output="Test expected output", + agent=test_agent, + ) + + future = task.execute_async() + + with pytest.raises(RuntimeError): + future.result(timeout=5) + + assert task.end_time is not None + + @patch("crewai.Agent.execute_task") + def test_execute_async_exception_does_not_hang( + self, mock_execute: MagicMock, test_agent: Agent + ) -> None: + """Test that execute_async does not hang when an exception occurs. + + This test verifies that the Future is properly completed with an exception, + allowing future.result() to return immediately instead of blocking forever. + """ + mock_execute.side_effect = Exception("Task execution failed") + task = Task( + description="Test task description", + expected_output="Test expected output", + agent=test_agent, + ) + + future = task.execute_async() + + with pytest.raises(Exception) as exc_info: + future.result(timeout=1) + + assert "Task execution failed" in str(exc_info.value)