mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-15 11:58:31 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user