Skip to content

Commit 54287ce

Browse files
maciejmajekJuliaj
authored andcommitted
feat: timeout
1 parent 9100040 commit 54287ce

File tree

4 files changed

+182
-18
lines changed

4 files changed

+182
-18
lines changed

src/rai_core/rai/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
get_llm_model_direct,
2121
get_tracing_callbacks,
2222
)
23+
from .utils import timeout
2324

2425
__all__ = [
2526
"AgentRunner",
@@ -29,4 +30,5 @@
2930
"get_llm_model_config_and_vendor",
3031
"get_llm_model_direct",
3132
"get_tracing_callbacks",
33+
"timeout",
3234
]

src/rai_core/rai/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
15+
from .timeout import timeout, timeout_method

src/rai_core/rai/tools/ros2/detection/tools.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from pydantic import BaseModel, Field
1818

19+
from rai.tools import timeout
1920
from rai.tools.ros2.base import BaseROS2Tool
2021
from rai.tools.ros2.detection.pcl import (
2122
GrippingPointEstimator,
@@ -40,24 +41,44 @@ class GetGrippingPointTool(BaseROS2Tool):
4041
gripping_point_estimator: GrippingPointEstimator
4142
point_cloud_filter: PointCloudFilter
4243

44+
timeout_sec: float = Field(
45+
default=10.0, description="Timeout in seconds to get the gripping point"
46+
)
47+
4348
args_schema: Type[GetGrippingPointToolInput] = GetGrippingPointToolInput
4449

4550
def _run(self, object_name: str) -> str:
46-
pcl = self.point_cloud_from_segmentation.run(object_name)
47-
pcl = self.point_cloud_filter.run(pcl)
48-
gps = self.gripping_point_estimator.run(pcl)
49-
50-
message = ""
51-
if len(gps) == 0:
52-
message += f"No gripping point found for the object {object_name}\n"
53-
elif len(gps) == 1:
54-
message += f"The gripping point of the object {object_name} is {gps[0]}\n"
55-
else:
56-
message += f"Multiple gripping points found for the object {object_name}\n"
57-
58-
for i, gp in enumerate(gps):
59-
message += (
60-
f"The gripping point of the object {i + 1} {object_name} is {gp}\n"
61-
)
62-
63-
return message
51+
@timeout(
52+
self.timeout_sec,
53+
f"Gripping point detection for object '{object_name}' exceeded {self.timeout_sec} seconds",
54+
)
55+
def _run_with_timeout():
56+
pcl = self.point_cloud_from_segmentation.run(object_name)
57+
pcl = self.point_cloud_filter.run(pcl)
58+
gps = self.gripping_point_estimator.run(pcl)
59+
60+
message = ""
61+
if len(gps) == 0:
62+
message += f"No gripping point found for the object {object_name}\n"
63+
elif len(gps) == 1:
64+
message += (
65+
f"The gripping point of the object {object_name} is {gps[0]}\n"
66+
)
67+
else:
68+
message += (
69+
f"Multiple gripping points found for the object {object_name}\n"
70+
)
71+
72+
for i, gp in enumerate(gps):
73+
message += (
74+
f"The gripping point of the object {i + 1} {object_name} is {gp}\n"
75+
)
76+
77+
return message
78+
79+
try:
80+
return _run_with_timeout()
81+
except Exception as e:
82+
if "timed out" in str(e).lower():
83+
return f"Timeout: Gripping point detection for object '{object_name}' exceeded {self.timeout_sec} seconds"
84+
raise

src/rai_core/rai/tools/timeout.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright (C) 2025 Robotec.AI
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import signal
16+
from functools import wraps
17+
from typing import Any, Callable, TypeVar
18+
19+
F = TypeVar("F", bound=Callable[..., Any])
20+
21+
22+
class TimeoutError(Exception):
23+
"""Raised when an operation times out."""
24+
25+
pass
26+
27+
28+
def timeout(seconds: float, timeout_message: str = None) -> Callable[[F], F]:
29+
"""
30+
Decorator that adds timeout functionality to a function.
31+
32+
Parameters
33+
----------
34+
seconds : float
35+
Timeout duration in seconds
36+
timeout_message : str, optional
37+
Custom timeout message. If not provided, a default message will be used.
38+
39+
Returns
40+
-------
41+
Callable
42+
Decorated function with timeout functionality
43+
44+
Raises
45+
------
46+
TimeoutError
47+
When the decorated function exceeds the specified timeout
48+
49+
Examples
50+
--------
51+
>>> @timeout(5.0, "Operation timed out")
52+
... def slow_operation():
53+
... import time
54+
... time.sleep(10)
55+
... return "Done"
56+
>>>
57+
>>> try:
58+
... result = slow_operation()
59+
... except TimeoutError as e:
60+
... print(f"Timeout: {e}")
61+
"""
62+
63+
def decorator(func: F) -> F:
64+
@wraps(func)
65+
def wrapper(*args, **kwargs):
66+
def timeout_handler(signum, frame):
67+
message = (
68+
timeout_message
69+
or f"Function '{func.__name__}' timed out after {seconds} seconds"
70+
)
71+
raise TimeoutError(message)
72+
73+
# Set up timeout
74+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
75+
signal.alarm(int(seconds))
76+
77+
try:
78+
return func(*args, **kwargs)
79+
finally:
80+
# Clean up timeout
81+
signal.alarm(0)
82+
signal.signal(signal.SIGALRM, old_handler)
83+
84+
return wrapper
85+
86+
return decorator
87+
88+
89+
def timeout_method(seconds: float, timeout_message: str = None) -> Callable[[F], F]:
90+
"""
91+
Decorator that adds timeout functionality to a method.
92+
Similar to timeout but designed for class methods.
93+
94+
Parameters
95+
----------
96+
seconds : float
97+
Timeout duration in seconds
98+
timeout_message : str, optional
99+
Custom timeout message. If not provided, a default message will be used.
100+
101+
Returns
102+
-------
103+
Callable
104+
Decorated method with timeout functionality
105+
106+
Examples
107+
--------
108+
>>> class MyClass:
109+
... @timeout_method(3.0, "Method timed out")
110+
... def slow_method(self):
111+
... import time
112+
... time.sleep(5)
113+
... return "Done"
114+
"""
115+
116+
def decorator(func: F) -> F:
117+
@wraps(func)
118+
def wrapper(self, *args, **kwargs):
119+
def timeout_handler(signum, frame):
120+
message = (
121+
timeout_message
122+
or f"Method '{func.__name__}' of {self.__class__.__name__} timed out after {seconds} seconds"
123+
)
124+
raise TimeoutError(message)
125+
126+
# Set up timeout
127+
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
128+
signal.alarm(int(seconds))
129+
130+
try:
131+
return func(self, *args, **kwargs)
132+
finally:
133+
# Clean up timeout
134+
signal.alarm(0)
135+
signal.signal(signal.SIGALRM, old_handler)
136+
137+
return wrapper
138+
139+
return decorator

0 commit comments

Comments
 (0)