fix: properly complete Future when async task execution fails

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 <joao@crewai.com>
This commit is contained in:
Devin AI
2025-12-11 14:19:23 +00:00
parent 8ef9fe2cab
commit d2c4040dd6
2 changed files with 96 additions and 2 deletions

View File

@@ -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(

View File

@@ -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"
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)