sunholo 0.141.1__py3-none-any.whl → 0.142.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,39 @@ from tenacity import AsyncRetrying, retry_if_exception_type, wait_random_exponen
8
8
  log = setup_logging("sunholo_AsyncTaskRunner")
9
9
 
10
10
  class AsyncTaskRunner:
11
- def __init__(self, retry_enabled:bool=False, retry_kwargs:dict=None, timeout:int=120, max_concurrency:int=20):
11
+ def __init__(self,
12
+ retry_enabled: bool = False,
13
+ retry_kwargs: dict = None,
14
+ timeout: int = 120,
15
+ max_concurrency: int = 20,
16
+ heartbeat_extends_timeout: bool = False,
17
+ hard_timeout: int = None):
18
+ """
19
+ Initialize AsyncTaskRunner with configurable timeout behavior.
20
+
21
+ Args:
22
+ retry_enabled: Whether to enable retries
23
+ retry_kwargs: Retry configuration
24
+ timeout: Base timeout for tasks (seconds)
25
+ max_concurrency: Maximum concurrent tasks
26
+ heartbeat_extends_timeout: If True, heartbeats reset the timeout timer
27
+ hard_timeout: Maximum absolute timeout regardless of heartbeats (seconds).
28
+ If None, defaults to timeout * 5 when heartbeat_extends_timeout=True
29
+ """
12
30
  self.tasks = []
13
31
  self.retry_enabled = retry_enabled
14
32
  self.retry_kwargs = retry_kwargs or {}
15
33
  self.timeout = timeout
16
34
  self.semaphore = asyncio.Semaphore(max_concurrency)
35
+ self.heartbeat_extends_timeout = heartbeat_extends_timeout
36
+
37
+ # Set hard timeout
38
+ if hard_timeout is not None:
39
+ self.hard_timeout = hard_timeout
40
+ elif heartbeat_extends_timeout:
41
+ self.hard_timeout = timeout * 5 # Default to 5x base timeout
42
+ else:
43
+ self.hard_timeout = timeout # Same as regular timeout
17
44
 
18
45
  def add_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any):
19
46
  """
@@ -39,9 +66,11 @@ class AsyncTaskRunner:
39
66
  for name, func, args, kwargs in self.tasks:
40
67
  log.info(f"Executing task: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
41
68
  completion_event = asyncio.Event()
42
- task_coro = self._run_with_retries_and_timeout(name, func, args, kwargs, queue, completion_event)
69
+ last_heartbeat = {'time': time.time()} # Shared mutable object for heartbeat tracking
70
+
71
+ task_coro = self._run_with_retries_and_timeout(name, func, args, kwargs, queue, completion_event, last_heartbeat)
43
72
  task = asyncio.create_task(task_coro)
44
- heartbeat_coro = self._send_heartbeat(name, completion_event, queue)
73
+ heartbeat_coro = self._send_heartbeat(name, completion_event, queue, last_heartbeat)
45
74
  heartbeat_task = asyncio.create_task(heartbeat_coro)
46
75
  task_infos.append({
47
76
  'name': name,
@@ -93,9 +122,12 @@ class AsyncTaskRunner:
93
122
  args: tuple,
94
123
  kwargs: dict,
95
124
  queue: asyncio.Queue,
96
- completion_event: asyncio.Event) -> None:
125
+ completion_event: asyncio.Event,
126
+ last_heartbeat: dict) -> None:
97
127
  try:
98
128
  log.info(f"run_with_retries_and_timeout: {name=}, {func=} with args: {args}, kwargs: {kwargs}")
129
+ log.info(f"Timeout mode: heartbeat_extends_timeout={self.heartbeat_extends_timeout}, timeout={self.timeout}s, hard_timeout={self.hard_timeout}s")
130
+
99
131
  if self.retry_enabled:
100
132
  retry_kwargs = {
101
133
  'wait': wait_random_exponential(multiplier=1, max=60),
@@ -106,13 +138,13 @@ class AsyncTaskRunner:
106
138
  async for attempt in AsyncRetrying(**retry_kwargs):
107
139
  with attempt:
108
140
  log.info(f"Starting task '{name}' with retry")
109
- result = await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
141
+ result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
110
142
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
111
143
  log.info(f"Sent 'task_complete' message for task '{name}'")
112
144
  return
113
145
  else:
114
146
  log.info(f"Starting task '{name}' with no retry")
115
- result = await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
147
+ result = await self._execute_task_with_timeout(func, name, last_heartbeat, *args, **kwargs)
116
148
  await queue.put({'type': 'task_complete', 'func_name': name, 'result': result})
117
149
  log.info(f"Sent 'task_complete' message for task '{name}'")
118
150
  except asyncio.TimeoutError:
@@ -125,6 +157,55 @@ class AsyncTaskRunner:
125
157
  log.info(f"Task '{name}' completed.")
126
158
  completion_event.set()
127
159
 
160
+ async def _execute_task_with_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
161
+ """
162
+ Execute task with either fixed timeout or heartbeat-extendable timeout.
163
+ """
164
+ if not self.heartbeat_extends_timeout:
165
+ # Original behavior - fixed timeout
166
+ return await asyncio.wait_for(self._execute_task(func, *args, **kwargs), timeout=self.timeout)
167
+ else:
168
+ # New behavior - heartbeat extends timeout
169
+ return await self._execute_task_with_heartbeat_timeout(func, name, last_heartbeat, *args, **kwargs)
170
+
171
+ async def _execute_task_with_heartbeat_timeout(self, func: Callable[..., Any], name: str, last_heartbeat: dict, *args: Any, **kwargs: Any) -> Any:
172
+ """
173
+ Execute task with heartbeat-extendable timeout and hard timeout limit.
174
+ """
175
+ start_time = time.time()
176
+ task = asyncio.create_task(self._execute_task(func, *args, **kwargs))
177
+
178
+ while not task.done():
179
+ current_time = time.time()
180
+
181
+ # Check hard timeout first (absolute limit)
182
+ if current_time - start_time > self.hard_timeout:
183
+ task.cancel()
184
+ try:
185
+ await task
186
+ except asyncio.CancelledError:
187
+ pass
188
+ raise asyncio.TimeoutError(f"Hard timeout exceeded ({self.hard_timeout}s)")
189
+
190
+ # Check soft timeout (extends with heartbeats)
191
+ time_since_heartbeat = current_time - last_heartbeat['time']
192
+ if time_since_heartbeat > self.timeout:
193
+ task.cancel()
194
+ try:
195
+ await task
196
+ except asyncio.CancelledError:
197
+ pass
198
+ raise asyncio.TimeoutError(f"Timeout exceeded - no heartbeat for {self.timeout}s")
199
+
200
+ # Wait a bit before checking again
201
+ try:
202
+ await asyncio.wait_for(asyncio.shield(task), timeout=1.0)
203
+ break # Task completed
204
+ except asyncio.TimeoutError:
205
+ continue # Check timeouts again
206
+
207
+ return await task
208
+
128
209
  async def _execute_task(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
129
210
  """
130
211
  Executes the given task function and returns its result.
@@ -143,14 +224,16 @@ class AsyncTaskRunner:
143
224
  else:
144
225
  return await asyncio.to_thread(func, *args, **kwargs)
145
226
 
146
- async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, interval: int = 2):
227
+ async def _send_heartbeat(self, func_name: str, completion_event: asyncio.Event, queue: asyncio.Queue, last_heartbeat: dict, interval: int = 2):
147
228
  """
148
229
  Sends periodic heartbeat updates to indicate the task is still in progress.
230
+ Updates last_heartbeat time if heartbeat_extends_timeout is enabled.
149
231
 
150
232
  Args:
151
233
  func_name (str): The name of the task function.
152
234
  completion_event (asyncio.Event): Event to signal when the task is completed.
153
235
  queue (asyncio.Queue): The queue to send heartbeat messages to.
236
+ last_heartbeat (dict): Mutable dict containing the last heartbeat time.
154
237
  interval (int): How frequently to send heartbeat messages (in seconds).
155
238
  """
156
239
  start_time = time.time()
@@ -158,7 +241,14 @@ class AsyncTaskRunner:
158
241
  try:
159
242
  while not completion_event.is_set():
160
243
  await asyncio.sleep(interval)
161
- elapsed_time = int(time.time() - start_time)
244
+ current_time = time.time()
245
+ elapsed_time = int(current_time - start_time)
246
+
247
+ # Update last heartbeat time if heartbeat extends timeout
248
+ if self.heartbeat_extends_timeout:
249
+ last_heartbeat['time'] = current_time
250
+ log.debug(f"Updated heartbeat time for task '{func_name}' at {current_time}")
251
+
162
252
  heartbeat_message = {
163
253
  'type': 'heartbeat',
164
254
  'name': func_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunholo
3
- Version: 0.141.1
3
+ Version: 0.142.0
4
4
  Summary: AI DevOps - a package to help deploy GenAI to the Cloud.
5
5
  Author-email: Holosun ApS <multivac@sunholo.com>
6
6
  License: Apache License, Version 2.0
@@ -96,7 +96,7 @@ sunholo/genai/init.py,sha256=yG8E67TduFCTQPELo83OJuWfjwTnGZsyACospahyEaY,687
96
96
  sunholo/genai/process_funcs_cls.py,sha256=D6eNrc3vtTZzwdkacZNOSfit499N_o0C5AHspyUJiYE,33690
97
97
  sunholo/genai/safety.py,sha256=mkFDO_BeEgiKjQd9o2I4UxB6XI7a9U-oOFjZ8LGRUC4,1238
98
98
  sunholo/invoke/__init__.py,sha256=o1RhwBGOtVK0MIdD55fAIMCkJsxTksi8GD5uoqVKI-8,184
99
- sunholo/invoke/async_class.py,sha256=OmRHaRf_yXNDWftWPpziytXh1TAhMumxnNhN_PGpQFs,8230
99
+ sunholo/invoke/async_class.py,sha256=ZMzxKQtelbYibu9Fac7P9OU3GorH8KxawZxSMv5EO9A,12514
100
100
  sunholo/invoke/direct_vac_func.py,sha256=dACx3Zh7uZnuWLIFYiyLoyXUhh5-eUpd2RatDUd9ov8,9753
101
101
  sunholo/invoke/invoke_vac_utils.py,sha256=sJc1edHTHMzMGXjji1N67c3iUaP7BmAL5nj82Qof63M,2053
102
102
  sunholo/langfuse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -169,9 +169,9 @@ sunholo/vertex/init.py,sha256=1OQwcPBKZYBTDPdyU7IM4X4OmiXLdsNV30C-fee2scQ,2875
169
169
  sunholo/vertex/memory_tools.py,sha256=tBZxqVZ4InTmdBvLlOYwoSEWu4-kGquc-gxDwZCC4FA,7667
170
170
  sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
171
171
  sunholo/vertex/type_dict_to_json.py,sha256=uTzL4o9tJRao4u-gJOFcACgWGkBOtqACmb6ihvCErL8,4694
172
- sunholo-0.141.1.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
173
- sunholo-0.141.1.dist-info/METADATA,sha256=Ett7hxu4MPWMTgGQyVNADbvuuPOWvXIVUl2aRLXgNn8,18292
174
- sunholo-0.141.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
175
- sunholo-0.141.1.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
176
- sunholo-0.141.1.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
177
- sunholo-0.141.1.dist-info/RECORD,,
172
+ sunholo-0.142.0.dist-info/licenses/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
173
+ sunholo-0.142.0.dist-info/METADATA,sha256=9v1AM0UCFJv_qx8vYOFO8gprTDulHCAxnMck7Jcmszs,18292
174
+ sunholo-0.142.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
175
+ sunholo-0.142.0.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
176
+ sunholo-0.142.0.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
177
+ sunholo-0.142.0.dist-info/RECORD,,