Skip to content

Commit 4639148

Browse files
committed
wip
1 parent e589c8a commit 4639148

File tree

3 files changed

+75
-31
lines changed

3 files changed

+75
-31
lines changed

hcloud/_utils.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from __future__ import annotations
22

3+
import time
34
from collections.abc import Iterable, Iterator
45
from itertools import islice
5-
from typing import TypeVar
6-
from functools import wraps
7-
from concurrent.futures import ThreadPoolExecutor
6+
from typing import Callable, TypeVar
87

98
T = TypeVar("T")
109

@@ -21,16 +20,41 @@ def batched(iterable: Iterable[T], size: int) -> Iterator[tuple[T, ...]]:
2120
yield batch
2221

2322

24-
def with_timeout(seconds: float | None):
25-
def decorator(f):
26-
@wraps(f)
27-
def wrapper(*args, **kwargs):
28-
if seconds:
29-
with ThreadPoolExecutor(max_workers=1) as executor:
30-
future = executor.submit(f, *args, **kwargs)
31-
return future.result(timeout=seconds)
32-
else:
33-
return f(*args, **kwargs)
23+
def waiter(timeout: float | None = None) -> Callable[[float], bool]:
24+
"""
25+
Waiter returns a wait function that sleeps the specified amount of seconds, and
26+
handles timeouts.
27+
28+
The wait function returns True if the timeout was reached, False otherwise.
29+
30+
:param timeout: Timeout in seconds, defaults to None.
31+
:return: Wait function.
32+
"""
33+
34+
if timeout:
35+
deadline = time.time() + timeout
36+
37+
def wait(seconds: float) -> bool:
38+
now = time.time()
39+
40+
# Timeout if the deadline exceeded.
41+
if deadline < now:
42+
return True
43+
44+
# The deadline is not exceeded after the sleep time.
45+
if now + seconds < deadline:
46+
time.sleep(seconds)
47+
return False
48+
49+
# The deadline is exceeded after the sleep time, clamp sleep time to
50+
# deadline, and allow one last attempt until next wait call.
51+
time.sleep(deadline - now)
52+
return False
53+
54+
else:
55+
56+
def wait(seconds: float) -> bool:
57+
time.sleep(seconds)
58+
return False
3459

35-
return wrapper
36-
return decorator
60+
return wait

hcloud/actions/client.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
from __future__ import annotations
22

3-
import time
43
import warnings
5-
from concurrent.futures import ThreadPoolExecutor
6-
from threading import Event
74
from typing import TYPE_CHECKING, Any, Callable, NamedTuple
85

9-
from .._utils import batched, with_timeout
6+
from .._utils import batched, waiter
107
from ..core import BoundModelBase, ClientEntityBase, Meta
118
from .domain import Action, ActionFailedException, ActionTimeoutException
129

@@ -19,18 +16,24 @@ class BoundAction(BoundModelBase, Action):
1916

2017
model = Action
2118

22-
def wait_until_finished(self, max_retries: int | None = None) -> None:
19+
def wait_until_finished(
20+
self,
21+
max_retries: int | None = None,
22+
*,
23+
timeout: float | None = None,
24+
) -> None:
2325
"""Wait until the specific action has status=finished.
2426
2527
:param max_retries: int Specify how many retries will be performed before an ActionTimeoutException will be raised.
2628
:raises: ActionFailedException when action is finished with status==error
27-
:raises: ActionTimeoutException when Action is still in status==running after max_retries is reached.
29+
:raises: ActionTimeoutException when Action is still in status==running after max_retries or timeout is reached.
2830
"""
2931
if max_retries is None:
3032
# pylint: disable=protected-access
3133
max_retries = self._client._client._poll_max_retries
3234

3335
retries = 0
36+
wait = waiter(timeout)
3437
while True:
3538
self.reload()
3639
if self.status != Action.STATUS_RUNNING:
@@ -39,8 +42,8 @@ def wait_until_finished(self, max_retries: int | None = None) -> None:
3942
retries += 1
4043
if retries < max_retries:
4144
# pylint: disable=protected-access
42-
time.sleep(self._client._client._poll_interval_func(retries))
43-
continue
45+
if not wait(self._client._client._poll_interval_func(retries)):
46+
continue
4447

4548
raise ActionTimeoutException(action=self)
4649

@@ -170,9 +173,10 @@ def _get_list_by_ids(self, ids: list[int]) -> list[BoundAction]:
170173

171174
def wait_for_function(
172175
self,
173-
stop: Event,
174176
handle_update: Callable[[BoundAction], None],
175177
actions: list[Action | BoundAction],
178+
*,
179+
timeout: float | None = None,
176180
) -> list[BoundAction]:
177181
"""
178182
Waits until all Actions succeed by polling the API at the interval defined by
@@ -183,6 +187,7 @@ def wait_for_function(
183187
184188
:param handle_update: Function called every time an Action is updated.
185189
:param actions: List of Actions to wait for.
190+
:param timeout: Timeout in seconds.
186191
:raises: ActionFailedException when an Action failed.
187192
:return: List of succeeded Actions.
188193
"""
@@ -191,10 +196,12 @@ def wait_for_function(
191196
completed: list[BoundAction] = []
192197

193198
retries = 0
199+
wait = waiter(timeout)
194200
while len(running_ids):
195201
# pylint: disable=protected-access
196-
if stop.wait(self._client._poll_interval_func(retries)):
197-
raise ActionTimeoutException()
202+
if wait(self._client._poll_interval_func(retries)):
203+
# TODO: How to raise a timeout exception for many actiosn without exception group.
204+
raise ActionTimeoutException("")
198205

199206
retries += 1
200207

@@ -212,6 +219,7 @@ def wait_for_function(
212219
def wait_for(
213220
self,
214221
actions: list[Action | BoundAction],
222+
*,
215223
timeout: float | None = None,
216224
) -> list[BoundAction]:
217225
"""
@@ -222,6 +230,7 @@ def wait_for(
222230
If a single Action fails, the function will stop waiting and raise ActionFailedException.
223231
224232
:param actions: List of Actions to wait for.
233+
:param timeout: Timeout in seconds.
225234
:raises: ActionFailedException when an Action failed.
226235
:raises: TimeoutError when the Actions did not succeed before timeout.
227236
:return: List of succeeded Actions.
@@ -231,10 +240,7 @@ def handle_update(update: BoundAction) -> None:
231240
if update.status == Action.STATUS_ERROR:
232241
raise ActionFailedException(action=update)
233242

234-
def run() -> list[BoundAction]:
235-
return self.wait_for_function(handle_update, actions)
236-
237-
return with_timeout(timeout)(run)()
243+
return self.wait_for_function(handle_update, actions, timeout=timeout)
238244

239245
def get_list(
240246
self,

tests/unit/test_utils.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
from __future__ import annotations
22

3-
from hcloud._utils import batched
3+
import time
4+
5+
from hcloud._utils import batched, waiter
46

57

68
def test_batched():
79
assert list(o for o in batched([1, 2, 3, 4, 5], 2)) == [(1, 2), (3, 4), (5,)]
10+
11+
12+
def test_waiter():
13+
wait = waiter(timeout=0.2)
14+
assert wait(0.1) is False
15+
time.sleep(0.2)
16+
assert wait(1) is True
17+
18+
# Clamp sleep to deadline
19+
wait = waiter(timeout=0.2)
20+
assert wait(0.3) is False
21+
assert wait(1) is True

0 commit comments

Comments
 (0)