Skip to content

Commit fa0eeb0

Browse files
Add global start timeout (#138)
1 parent 077f1e8 commit fa0eeb0

File tree

3 files changed

+61
-2
lines changed

3 files changed

+61
-2
lines changed

src/fps/_module.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections.abc import Callable, Awaitable
77
from contextlib import AsyncExitStack
88
from inspect import isawaitable, signature, _empty
9+
from time import time
910
from typing import TypeVar, Any, Iterable, cast
1011

1112
import anyio
@@ -56,18 +57,22 @@ def __init__(
5657
prepare_timeout: float = 1,
5758
start_timeout: float = 1,
5859
stop_timeout: float = 1,
60+
global_start_timeout: float | None = None,
5961
):
6062
"""
6163
Args:
6264
name: The name to give to the module.
6365
prepare_timeout: The time to wait (in seconds) for the "prepare" phase to complete.
6466
start_timeout: The time to wait (in seconds) for the "start" phase to complete.
6567
stop_timeout: The time to wait (in seconds) for the "stop" phase to complete.
68+
global_start_timeout: The time to wait (in seconds) for the "prepare" and "start"
69+
phases to complete.
6670
"""
6771
self._initialized = False
6872
self._prepare_timeout = prepare_timeout
6973
self._start_timeout = start_timeout
7074
self._stop_timeout = stop_timeout
75+
self._global_start_timeout = global_start_timeout
7176
self._parent: Module | None = None
7277
self._context = Context()
7378
self._prepared = Event()
@@ -327,7 +332,12 @@ async def __aenter__(self) -> Module:
327332
self._task_group = await exit_stack.enter_async_context(create_task_group())
328333
self._exceptions = []
329334
self._phase = "preparing"
330-
with move_on_after(self._prepare_timeout) as scope:
335+
if self._global_start_timeout is None:
336+
prepare_timeout = self._prepare_timeout
337+
else:
338+
prepare_timeout = self._global_start_timeout
339+
t0 = time()
340+
with move_on_after(prepare_timeout) as scope:
331341
self._task_group.start_soon(self._prepare, name=f"{self.path} _prepare")
332342
await self._all_prepared()
333343
if scope.cancelled_caught:
@@ -336,7 +346,12 @@ async def __aenter__(self) -> Module:
336346
self._exit.set()
337347
else:
338348
self._phase = "starting"
339-
with move_on_after(self._start_timeout) as scope:
349+
if self._global_start_timeout is None:
350+
start_timeout = self._start_timeout
351+
else:
352+
elapsed_time = time() - t0
353+
start_timeout = max(self._global_start_timeout - elapsed_time, 0)
354+
with move_on_after(start_timeout) as scope:
340355
self._task_group.start_soon(self._start, name=f"{self.path} start")
341356
await self._all_started()
342357
if scope.cancelled_caught:

src/fps/cli/_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@
4343
default="asyncio",
4444
help="The name of the event loop to use (asyncio or trio).",
4545
)
46+
@click.option(
47+
"--timeout",
48+
type=float,
49+
default=None,
50+
help="The timeout for starting the module (in seconds).",
51+
)
52+
@click.option(
53+
"--stop-timeout",
54+
type=float,
55+
show_default=True,
56+
default=1,
57+
help="The timeout for stopping the module (in seconds).",
58+
)
4659
@click.argument("module", default="")
4760
def main(
4861
module: str,
@@ -51,6 +64,8 @@ def main(
5164
help_all: bool = False,
5265
set_: list[str] | None = None,
5366
backend: str = "asyncio",
67+
timeout: float | None = None,
68+
stop_timeout: float = 1,
5469
):
5570
global CONFIG
5671
if config is None:
@@ -87,6 +102,8 @@ def main(
87102
CONFIG = config_dict
88103
return
89104
root_module = get_root_module(config_dict)
105+
root_module._global_start_timeout = timeout
106+
root_module._stop_timeout = stop_timeout
90107
actual_config = initialize(root_module)
91108
if help_all:
92109
click.echo(get_config_description(root_module))

tests/test_start_stop.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,30 @@ def __init__(self, name, stop_timeout):
108108
str(module0.exceptions[0])
109109
== "Module timed out while stopping: module0.submodule0"
110110
)
111+
112+
113+
async def test_global_start_timeout_in_prepare():
114+
class Module0(Module):
115+
async def prepare(self):
116+
await sleep(1)
117+
118+
async with Module0("module0", global_start_timeout=0.1) as module0:
119+
pass
120+
121+
assert len(module0.exceptions) == 1
122+
assert str(module0.exceptions[0]) == "Module timed out while preparing: module0"
123+
124+
125+
async def test_global_start_timeout_in_start():
126+
class Module0(Module):
127+
async def prepare(self):
128+
await sleep(0.1)
129+
130+
async def start(self):
131+
await sleep(1)
132+
133+
async with Module0("module0", global_start_timeout=0.2) as module0:
134+
pass
135+
136+
assert len(module0.exceptions) == 1
137+
assert str(module0.exceptions[0]) == "Module timed out while starting: module0"

0 commit comments

Comments
 (0)