Skip to content

Commit ffdceec

Browse files
committed
feat(launcher): update product_instance.py
1 parent cffcae0 commit ffdceec

File tree

1 file changed

+53
-57
lines changed

1 file changed

+53
-57
lines changed

src/ansys/tools/common/launcher/product_instance.py

Lines changed: 53 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -25,40 +25,45 @@
2525
from __future__ import annotations
2626

2727
import time
28-
from typing import Any
28+
from typing import Any, Mapping
2929
import weakref
30+
from types import MappingProxyType
3031

3132
import grpc
3233
from typing_extensions import Self
3334

3435
from .interface import LAUNCHER_CONFIG_T, LauncherProtocol, ServerType
3536

36-
__all__ = ["ProductInstance"]
37+
__all__ = ["ProductInstance", "ProductInstanceError"]
3738

3839
_GRPC_MAX_MESSAGE_LENGTH = 256 * 1024**2 # 256 MB
3940

4041

42+
class ProductInstanceError(RuntimeError):
43+
"""Custom exception for ProductInstance lifecycle errors."""
44+
45+
4146
class ProductInstance:
4247
"""Provides a wrapper for interacting with the launched product instance.
4348
4449
This class allows stopping and starting of the product instance. It also
45-
provides access to its server URLs/channels.
50+
provides access to its server URLs and gRPC channels.
4651
4752
The :class:`ProductInstance` class can be used as a context manager, stopping
4853
the instance when exiting the context.
4954
"""
5055

5156
def __init__(self, *, launcher: LauncherProtocol[LAUNCHER_CONFIG_T]):
5257
self._launcher = launcher
53-
self._finalizer: weakref.finalize[Any, Self]
54-
self._urls: dict[str, str]
55-
self._channels: dict[str, grpc.Channel]
58+
self._finalizer: weakref.finalize[Any, Self] | None = None
59+
self._urls: Mapping[str, str] | None = None
60+
self._channels: Mapping[str, grpc.Channel] | None = None
5661
self.start()
5762

5863
def __enter__(self) -> ProductInstance:
5964
"""Enter the context manager defined by the product instance."""
6065
if self.stopped:
61-
raise RuntimeError("The product instance is stopped. Cannot enter context.")
66+
raise ProductInstanceError("The product instance is stopped. Cannot enter context.")
6267
return self
6368

6469
def __exit__(self, *exc: Any) -> None:
@@ -70,28 +75,29 @@ def start(self: Self) -> None:
7075
7176
Raises
7277
------
73-
RuntimeError
74-
If the instance is already in the started state.
75-
RuntimeError
76-
If the URLs exposed by the started instance do not match
77-
the expected ones defined in the launcher's
78-
:attr:`.LauncherProtocol.SERVER_SPEC` attribute.
78+
ProductInstanceError
79+
If the instance is already started or the URLs do not match
80+
the launcher's SERVER_SPEC.
7981
"""
8082
if not self.stopped:
81-
raise RuntimeError("Cannot start the server. It has already been started.")
83+
raise ProductInstanceError("Cannot start the server. It has already been started.")
84+
8285
self._finalizer = weakref.finalize(self, self._launcher.stop, timeout=None)
8386
self._launcher.start()
84-
self._channels = dict()
85-
urls = self.urls
86-
if urls.keys() != self._launcher.SERVER_SPEC.keys():
87-
raise RuntimeError(
88-
f"The URL keys '{urls.keys()}' provided by the launcher "
89-
f"do not match the SERVER_SPEC keys '{self._launcher.SERVER_SPEC.keys()}'."
87+
88+
self._channels = {}
89+
self._urls = MappingProxyType(self._launcher.urls)
90+
91+
if self._urls.keys() != self._launcher.SERVER_SPEC.keys():
92+
raise ProductInstanceError(
93+
f"The URL keys '{self._urls.keys()}' provided by the launcher "
94+
f"do not match the SERVER_SPEC keys '{self._launcher.SERVER_SPEC.keys()}'"
9095
)
96+
9197
for key, server_type in self._launcher.SERVER_SPEC.items():
9298
if server_type == ServerType.GRPC:
9399
self._channels[key] = grpc.insecure_channel(
94-
urls[key],
100+
self._urls[key],
95101
options=[("grpc.max_receive_message_length", _GRPC_MAX_MESSAGE_LENGTH)],
96102
)
97103

@@ -102,18 +108,19 @@ def stop(self, *, timeout: float | None = None) -> None:
102108
----------
103109
timeout :
104110
Time in seconds after which the instance is forcefully stopped.
105-
Not all launch methods implement this parameter. If the parameter
106-
is not implemented, it is ignored.
111+
Not all launch methods implement this parameter.
107112
108113
Raises
109114
------
110-
RuntimeError
111-
If the instance is already in the stopped state.
115+
ProductInstanceError
116+
If the instance is already stopped.
112117
"""
113118
if self.stopped:
114-
raise RuntimeError("Cannot stop the server. It has already been stopped.")
119+
raise ProductInstanceError("Cannot stop the server. It has already been stopped.")
120+
115121
self._launcher.stop(timeout=timeout)
116-
self._finalizer.detach()
122+
if self._finalizer is not None:
123+
self._finalizer.detach()
117124

118125
def restart(self, stop_timeout: float | None = None) -> None:
119126
"""Stop and then start the product instance.
@@ -122,17 +129,11 @@ def restart(self, stop_timeout: float | None = None) -> None:
122129
----------
123130
stop_timeout :
124131
Time in seconds after which the instance is forcefully stopped.
125-
Not all launch methods implement this parameter. If the parameter
126-
is not implemented, it is ignored.
127132
128133
Raises
129134
------
130-
RuntimeError
131-
If the instance is already in the stopped state.
132-
RuntimeError
133-
If the URLs exposed by the started instance do not match
134-
the expected ones defined in the launcher's
135-
:attr:`.LauncherProtocol.SERVER_SPEC` attribute.
135+
ProductInstanceError
136+
If the instance is already stopped or URL keys mismatch.
136137
"""
137138
self.stop(timeout=stop_timeout)
138139
self.start()
@@ -143,9 +144,8 @@ def check(self, timeout: float | None = None) -> bool:
143144
Parameters
144145
----------
145146
timeout :
146-
Time in seconds to wait for the servers to respond. There
147-
is no guarantee that the ``check()`` method returns within this time.
148-
Instead, this parameter is used as a hint to the launcher implementation.
147+
Time in seconds to wait for the servers to respond. This is a hint
148+
to the launcher; the method may return earlier or later.
149149
"""
150150
return self._launcher.check(timeout=timeout)
151151

@@ -162,36 +162,32 @@ def wait(self, timeout: float) -> None:
162162
163163
Raises
164164
------
165-
RuntimeError
166-
If the server still has not responded after ``timeout`` seconds.
165+
ProductInstanceError
166+
If the server still has not responded after `timeout` seconds.
167167
"""
168168
start_time = time.time()
169169
while time.time() - start_time <= timeout:
170170
if self.check(timeout=timeout / 3):
171171
break
172-
else:
173-
# Try again until the timeout is reached. We add a small
174-
# delay s.t. the server isn't bombarded with requests.
175-
time.sleep(timeout / 100)
172+
time.sleep(max(timeout / 100, 0.01)) # minimum sleep to avoid busy waiting
176173
else:
177-
raise RuntimeError(f"The product is not running after {timeout}s.")
174+
raise ProductInstanceError(f"The product is not running after {timeout}s.")
178175

179176
@property
180-
def urls(self) -> dict[str, str]:
181-
"""URL and port for the servers of the product instance."""
182-
return self._launcher.urls
177+
def urls(self) -> Mapping[str, str]:
178+
"""Read-only mapping of server keys to their URLs."""
179+
if self._urls is None:
180+
return MappingProxyType({})
181+
return self._urls
183182

184183
@property
185184
def stopped(self) -> bool:
186185
"""Flag indicating if the product instance is currently stopped."""
187-
try:
188-
return not self._finalizer.alive
189-
# If the server has never been started, the '_finalizer' attribute
190-
# may not be defined.
191-
except AttributeError:
192-
return True
186+
return self._finalizer is None or not self._finalizer.alive
193187

194188
@property
195-
def channels(self) -> dict[str, grpc.Channel]:
196-
"""Channels to the gRPC servers of the product instance."""
197-
return self._channels
189+
def channels(self) -> Mapping[str, grpc.Channel]:
190+
"""Read-only mapping of server keys to gRPC channels."""
191+
if self._channels is None:
192+
return MappingProxyType({})
193+
return MappingProxyType(self._channels)

0 commit comments

Comments
 (0)