Skip to content

Commit ad02e3d

Browse files
committed
fix(social-algorithms): time out with a joined thread, not SIGALRM
`_execute_with_timeout` installed a `signal.SIGALRM` handler, and `signal.signal` only works on the main thread. `run_async` hands `run` to a worker thread via `asyncio.to_thread`, and `max_execution_time` defaults to 300.0, which selects the timeout path -- so the default configuration always raised `ValueError: signal only works in main thread of the main interpreter`. The same mechanism carried two more defects. `signal.alarm(int(...))` truncates, and `alarm(0)` means cancel, so any budget under one second disabled the timeout instead of tightening it. And `SIGALRM` does not exist on Windows, where the default path raised `AttributeError`. Run the call on a joined daemon thread instead. That works from any thread, keeps sub-second budgets, and is portable. A timed-out worker is abandoned rather than interrupted -- it is a daemon, so it cannot hold up interpreter exit. Closes kyegomez#2070
1 parent 99b4d14 commit ad02e3d

2 files changed

Lines changed: 44 additions & 14 deletions

File tree

swarms/structs/social_algorithms.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import threading
12
import time
23
import uuid
34
from typing import Any, Callable, Dict, List, Optional
@@ -287,24 +288,25 @@ def _execute_with_timeout(
287288
Raises:
288289
TimeoutError: If the function execution exceeds max_execution_time.
289290
"""
290-
import signal
291+
outcome: Dict[str, Any] = {}
291292

292-
def timeout_handler(signum, frame):
293+
def target() -> None:
294+
try:
295+
outcome["result"] = func(*args, **kwargs)
296+
except BaseException as exc:
297+
outcome["error"] = exc
298+
299+
worker = threading.Thread(target=target, daemon=True)
300+
worker.start()
301+
worker.join(self.max_execution_time)
302+
303+
if worker.is_alive():
293304
raise TimeoutError(
294305
f"Algorithm execution exceeded {self.max_execution_time} seconds"
295306
)
296-
297-
# Set up timeout
298-
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
299-
signal.alarm(int(self.max_execution_time))
300-
301-
try:
302-
result = func(*args, **kwargs)
303-
return result
304-
finally:
305-
# Restore original handler
306-
signal.alarm(0)
307-
signal.signal(signal.SIGALRM, old_handler)
307+
if "error" in outcome:
308+
raise outcome["error"]
309+
return outcome.get("result")
308310

309311
def _format_output(self, result: Any) -> Any:
310312
"""

tests/structs/test_social_algorithms.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
import pytest
24

35
from swarms.structs.agent import Agent
@@ -56,3 +58,29 @@ def test_remove_agent_raises_agent_not_found_for_empty_agent_list():
5658

5759
with pytest.raises(AgentNotFoundError):
5860
social_algorithm.remove_agent("researcher")
61+
62+
63+
def test_run_async_works_on_the_default_timeout_configuration():
64+
social_algorithm = SocialAlgorithms(
65+
agents=[_agent("worker")],
66+
social_algorithm=lambda agents, task, **kwargs: f"done:{task}",
67+
)
68+
69+
result = social_algorithm.run_async("t")
70+
71+
assert result.final_outputs == {"result": "done:t"}
72+
73+
74+
def test_sub_second_max_execution_time_still_times_out():
75+
def slow_algorithm(agents, task, **kwargs):
76+
time.sleep(5)
77+
return "never"
78+
79+
social_algorithm = SocialAlgorithms(
80+
agents=[_agent("worker")],
81+
social_algorithm=slow_algorithm,
82+
max_execution_time=0.1,
83+
)
84+
85+
with pytest.raises(TimeoutError):
86+
social_algorithm.run("t")

0 commit comments

Comments
 (0)