Skip to content

Commit d2c4040

Browse files
devin-ai-integration[bot]João
andcommitted
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 <[email protected]>
1 parent 8ef9fe2 commit d2c4040

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed

lib/crewai/src/crewai/task.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,11 @@ def _execute_task_async(
494494
future: Future[TaskOutput],
495495
) -> None:
496496
"""Execute the task asynchronously with context handling."""
497-
result = self._execute_core(agent, context, tools)
497+
try:
498+
result = self._execute_core(agent, context, tools)
499+
except Exception as e:
500+
future.set_exception(e)
501+
return
498502
future.set_result(result)
499503

500504
async def aexecute_sync(

lib/crewai/tests/task/test_async_task.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,4 +383,94 @@ async def test_aexecute_sync_task_output_attributes(
383383
assert result.description == "Test description"
384384
assert result.expected_output == "Test expected"
385385
assert result.raw == "Test result"
386-
assert result.agent == "Test Agent"
386+
assert result.agent == "Test Agent"
387+
388+
389+
class TestThreadedAsyncExecution:
390+
"""Tests for threaded async task execution (execute_async with Future)."""
391+
392+
@patch("crewai.Agent.execute_task")
393+
def test_execute_async_basic(
394+
self, mock_execute: MagicMock, test_agent: Agent
395+
) -> None:
396+
"""Test basic threaded async task execution."""
397+
mock_execute.return_value = "Async task result"
398+
task = Task(
399+
description="Test task description",
400+
expected_output="Test expected output",
401+
agent=test_agent,
402+
)
403+
404+
future = task.execute_async()
405+
result = future.result(timeout=5)
406+
407+
assert result is not None
408+
assert isinstance(result, TaskOutput)
409+
assert result.raw == "Async task result"
410+
assert result.agent == "Test Agent"
411+
mock_execute.assert_called_once()
412+
413+
@patch("crewai.Agent.execute_task")
414+
def test_execute_async_exception_completes_future(
415+
self, mock_execute: MagicMock, test_agent: Agent
416+
) -> None:
417+
"""Test that execute_async properly completes the Future when an exception occurs.
418+
419+
This is a regression test for GitHub issue #4072 where an async task that
420+
errors would keep its thread alive because the Future was never completed.
421+
"""
422+
mock_execute.side_effect = ValueError("Something happened here")
423+
task = Task(
424+
description="Test task description",
425+
expected_output="Test expected output",
426+
agent=test_agent,
427+
)
428+
429+
future = task.execute_async()
430+
431+
with pytest.raises(ValueError) as exc_info:
432+
future.result(timeout=5)
433+
434+
assert "Something happened here" in str(exc_info.value)
435+
436+
@patch("crewai.Agent.execute_task")
437+
def test_execute_async_exception_sets_end_time(
438+
self, mock_execute: MagicMock, test_agent: Agent
439+
) -> None:
440+
"""Test that execute_async sets end_time even when an exception occurs."""
441+
mock_execute.side_effect = RuntimeError("Test error")
442+
task = Task(
443+
description="Test task description",
444+
expected_output="Test expected output",
445+
agent=test_agent,
446+
)
447+
448+
future = task.execute_async()
449+
450+
with pytest.raises(RuntimeError):
451+
future.result(timeout=5)
452+
453+
assert task.end_time is not None
454+
455+
@patch("crewai.Agent.execute_task")
456+
def test_execute_async_exception_does_not_hang(
457+
self, mock_execute: MagicMock, test_agent: Agent
458+
) -> None:
459+
"""Test that execute_async does not hang when an exception occurs.
460+
461+
This test verifies that the Future is properly completed with an exception,
462+
allowing future.result() to return immediately instead of blocking forever.
463+
"""
464+
mock_execute.side_effect = Exception("Task execution failed")
465+
task = Task(
466+
description="Test task description",
467+
expected_output="Test expected output",
468+
agent=test_agent,
469+
)
470+
471+
future = task.execute_async()
472+
473+
with pytest.raises(Exception) as exc_info:
474+
future.result(timeout=1)
475+
476+
assert "Task execution failed" in str(exc_info.value)

0 commit comments

Comments
 (0)