Skip to content

Commit 1c00565

Browse files
build: bump grpcio limits and handle erratic gRPC channel creation (#1913)
Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent c5cd303 commit 1c00565

File tree

7 files changed

+108
-48
lines changed

7 files changed

+108
-48
lines changed

.github/workflows/ci_cd.yml

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,6 @@ jobs:
481481
username: ${{ github.actor }}
482482
password: ${{ secrets.GITHUB_TOKEN }}
483483

484-
- name: Pull and launch geometry service
485-
run: |
486-
docker pull ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
487-
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
488-
489484
- name: Checkout repository
490485
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
491486

@@ -500,6 +495,12 @@ jobs:
500495
pip install -e .[all,tests-minimal]
501496
pip install pytest
502497
498+
- name: Start Geometry service and verify start
499+
run: |
500+
docker pull ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
501+
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
502+
python -c "from ansys.geometry.core.connection.validate import validate; validate()"
503+
503504
- name: Run pytest
504505
run: |
505506
pytest -v
@@ -529,11 +530,6 @@ jobs:
529530
username: ${{ github.actor }}
530531
password: ${{ secrets.GITHUB_TOKEN }}
531532

532-
- name: Pull and launch geometry service
533-
run: |
534-
docker pull ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
535-
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
536-
537533
- name: Checkout repository
538534
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
539535

@@ -551,6 +547,12 @@ jobs:
551547
# Installing docker (needed for the tests)
552548
pip install docker
553549
550+
- name: Start Geometry service and verify start
551+
run: |
552+
docker pull ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
553+
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ${{ env.ANSRV_GEO_IMAGE_MINREQS }}
554+
python -c "from ansys.geometry.core.connection.validate import validate; validate()"
555+
554556
- name: Run pytest
555557
run: |
556558
pytest -v -c pytest-nographics.ini
@@ -562,7 +564,6 @@ jobs:
562564
docker logs ${{ env.GEO_CONT_NAME }}
563565
docker rm ${{ env.GEO_CONT_NAME }}
564566
565-
566567
package:
567568
name: Package library
568569
needs: [testing-windows, testing-linux, testing-min-reqs, testing-no-graphics, docs]
@@ -691,17 +692,12 @@ jobs:
691692
run:
692693
echo "ANSRV_GEO_LICENSE_SERVER=${{ secrets.INTERNAL_LICENSE_SERVER }}" | Out-File -FilePath $env:GITHUB_ENV -Append
693694

694-
- name: Launch Geometry service
695-
run: |
696-
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ghcr.io/ansys/geometry:windows-tmp
697-
698695
- name: Validate connection using PyAnsys Geometry
699696
run: |
700697
python -m venv .venv
701698
.\.venv\Scripts\Activate.ps1
702699
python -m pip install --upgrade pip
703700
pip install -e .[tests]
704-
python -c "from ansys.geometry.core.connection.validate import validate; validate()"
705701
706702
- name: Restore images cache
707703
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
@@ -710,6 +706,12 @@ jobs:
710706
key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }}
711707
restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}
712708

709+
- name: Start Geometry service and verify start
710+
run: |
711+
.\.venv\Scripts\Activate.ps1
712+
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ghcr.io/ansys/geometry:windows-tmp
713+
python -c "from ansys.geometry.core.connection.validate import validate; validate()"
714+
713715
- name: Testing
714716
run: |
715717
.\.venv\Scripts\Activate.ps1
@@ -778,14 +780,14 @@ jobs:
778780
run: |
779781
docker build -f linux/coreservice/Dockerfile -t ghcr.io/ansys/geometry:linux-tmp .
780782
781-
- name: Launch Geometry service
782-
run: |
783-
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ghcr.io/ansys/geometry:linux-tmp
784-
785783
- name: Validate connection using PyAnsys Geometry
786784
run: |
787785
python -m pip install --upgrade pip
788786
pip install -e .[tests]
787+
788+
- name: Start Geometry service and verify start
789+
run: |
790+
docker run --detach --name ${{ env.GEO_CONT_NAME }} -e LICENSE_SERVER=${{ env.ANSRV_GEO_LICENSE_SERVER }} -p ${{ env.ANSRV_GEO_PORT }}:50051 ghcr.io/ansys/geometry:linux-tmp
789791
python -c "from ansys.geometry.core.connection.validate import validate; validate()"
790792
791793
- name: Restore images cache

doc/changelog.d/1913.dependencies.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bump grpcio limits and handle erratic gRPC channel creation

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ dependencies = [
2828
"ansys-tools-path>=0.3,<1",
2929
"beartype>=0.11.0,<0.21",
3030
"geomdl>=5,<6",
31-
"grpcio>=1.35.0,<1.68",
32-
"grpcio-health-checking>=1.45.0,<1.68",
31+
"grpcio>=1.35.0,<2",
32+
"grpcio-health-checking>=1.45.0,<2",
3333
"matplotlib>=3,<4",
3434
"numpy>=1.20.3,<3",
3535
"Pint>=0.18,<1",
@@ -62,8 +62,8 @@ tests = [
6262
"beartype==0.20.2",
6363
"docker==7.1.0",
6464
"geomdl==5.3.1",
65-
"grpcio==1.67.1",
66-
"grpcio-health-checking==1.67.1",
65+
"grpcio==1.71.0",
66+
"grpcio-health-checking==1.71.0",
6767
"matplotlib==3.10.1",
6868
"numpy==2.2.4",
6969
"Pint==0.24.4",
@@ -93,8 +93,8 @@ doc = [
9393
"beartype==0.20.2",
9494
"docker==7.1.0",
9595
"geomdl==5.3.1",
96-
"grpcio==1.67.1",
97-
"grpcio-health-checking==1.67.1",
96+
"grpcio==1.71.0",
97+
"grpcio-health-checking==1.71.0",
9898
"ipyvtklink==0.2.3",
9999
"jupyter_sphinx==0.5.3",
100100
"jupytext==1.16.7",

src/ansys/geometry/core/_grpc/_services/base/admin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ def get_backend(self, **kwargs) -> dict:
4848
def get_logs(self, **kwargs) -> dict:
4949
"""Get server logs."""
5050
pass # pragma: no cover
51+
52+
@abstractmethod
53+
def get_service_status(self, **kwargs) -> dict:
54+
"""Get server status (i.e. healthy or not)."""
55+
pass # pragma: no cover

src/ansys/geometry/core/_grpc/_services/v0/admin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,16 @@ def get_logs(self, **kwargs) -> dict: # noqa: D102
104104
logs[chunk.log_name] += chunk.log_chunk.decode()
105105

106106
return {"logs": logs}
107+
108+
@protect_grpc
109+
def get_service_status(self, **kwargs) -> dict: # noqa: D102
110+
from google.protobuf.empty_pb2 import Empty
111+
112+
# Create the request - assumes all inputs are valid and of the proper type
113+
request = Empty()
114+
115+
# Call the gRPC service
116+
response = self.stub.Health(request=request)
117+
118+
# Convert the response to a dictionary
119+
return {"healthy": True if response.message == "I am healthy!" else False}

src/ansys/geometry/core/_grpc/_services/v1/admin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ def get_backend(self, **kwargs) -> dict: # noqa: D102
5454
@protect_grpc
5555
def get_logs(self, **kwargs) -> dict: # noqa: D102
5656
raise NotImplementedError
57+
58+
@protect_grpc
59+
def get_service_status(self, **kwargs) -> dict: # noqa: D102
60+
raise NotImplementedError

src/ansys/geometry/core/connection/client.py

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,41 @@
4848
pass
4949

5050

51-
def wait_until_healthy(channel: grpc.Channel, timeout: float):
51+
def _create_geometry_channel(target: str) -> grpc.Channel:
52+
"""Create a Geometry service gRPC channel.
53+
54+
Notes
55+
-----
56+
Contains specific options for the Geometry service.
57+
58+
Parameters
59+
----------
60+
target : str
61+
Target of the channel. This is usually a string in the form of
62+
``host:port``.
63+
64+
Returns
65+
-------
66+
~grpc.Channel
67+
gRPC channel for the Geometry service.
68+
"""
69+
return grpc.insecure_channel(
70+
target,
71+
options=[
72+
("grpc.max_receive_message_length", pygeom_defaults.MAX_MESSAGE_LENGTH),
73+
("grpc.max_send_message_length", pygeom_defaults.MAX_MESSAGE_LENGTH),
74+
],
75+
)
76+
77+
78+
def wait_until_healthy(channel: grpc.Channel | str, timeout: float) -> grpc.Channel:
5279
"""Wait until a channel is healthy before returning.
5380
5481
Parameters
5582
----------
56-
channel : ~grpc.Channel
57-
Channel that must be established and healthy.
83+
channel : ~grpc.Channel | str
84+
Channel that must be established and healthy. The target can also be
85+
passed in. In that case, a channel is created using the default insecure channel.
5886
timeout : float
5987
Timeout in seconds. Attempts are made with the following backoff strategy:
6088
@@ -70,14 +98,28 @@ def wait_until_healthy(channel: grpc.Channel, timeout: float):
7098
------
7199
TimeoutError
72100
Raised when the total elapsed time exceeds the value for the ``timeout`` parameter.
101+
102+
Returns
103+
-------
104+
grpc.Channel
105+
The channel that was passed in. This channel is guaranteed to be healthy.
106+
If a string was passed in, a channel is created using the default insecure channel.
73107
"""
74108
t_max = time.time() + timeout
75-
health_stub = health_pb2_grpc.HealthStub(channel)
76-
request = health_pb2.HealthCheckRequest(service="")
77-
78109
t_out = 0.1
110+
111+
# If the channel is a string, create a channel using the default insecure channel
112+
channel_creation_required = True if isinstance(channel, str) else False
113+
tmp_channel = None
114+
79115
while time.time() < t_max:
80116
try:
117+
tmp_channel = (
118+
_create_geometry_channel(channel) if channel_creation_required else channel
119+
)
120+
health_stub = health_pb2_grpc.HealthStub(tmp_channel)
121+
request = health_pb2.HealthCheckRequest(service="")
122+
81123
out = health_stub.Check(request, timeout=t_out)
82124
if out.status is health_pb2.HealthCheckResponse.SERVING:
83125
break
@@ -91,11 +133,13 @@ def wait_until_healthy(channel: grpc.Channel, timeout: float):
91133
t_out = t_max - t_now
92134
continue
93135
else:
94-
target_str = channel._channel.target().decode()
136+
target_str = tmp_channel._channel.target().decode()
95137
raise TimeoutError(
96138
f"Channel health check to target '{target_str}' timed out after {timeout} seconds."
97139
)
98140

141+
return tmp_channel
142+
99143

100144
class GrpcClient:
101145
"""Wraps the gRPC connection for the Geometry service.
@@ -155,23 +199,15 @@ def __init__(
155199
self._remote_instance = remote_instance
156200
self._docker_instance = docker_instance
157201
self._product_instance = product_instance
202+
self._grpc_health_timeout = timeout
203+
158204
if channel:
159205
# Used for PyPIM when directly providing a channel
160-
self._channel = channel
161206
self._target = str(channel)
207+
self._channel = wait_until_healthy(channel, self._grpc_health_timeout)
162208
else:
163209
self._target = f"{host}:{port}"
164-
self._channel = grpc.insecure_channel(
165-
self._target,
166-
options=[
167-
("grpc.max_receive_message_length", pygeom_defaults.MAX_MESSAGE_LENGTH),
168-
("grpc.max_send_message_length", pygeom_defaults.MAX_MESSAGE_LENGTH),
169-
],
170-
)
171-
172-
# do not finish initialization until channel is healthy
173-
self._grpc_health_timeout = timeout
174-
wait_until_healthy(self._channel, self._grpc_health_timeout)
210+
self._channel = wait_until_healthy(self._target, self._grpc_health_timeout)
175211

176212
# Initialize the gRPC services
177213
self._services = _GRPCServices(self._channel, version=proto_version)
@@ -263,9 +299,8 @@ def healthy(self) -> bool:
263299
if self._closed:
264300
return False
265301
try:
266-
wait_until_healthy(self._channel, self._grpc_health_timeout)
267-
return True
268-
except TimeoutError: # pragma: no cover
302+
return self.services.admin.get_service_status().get("healthy")
303+
except Exception: # pragma: no cover
269304
return False
270305

271306
def __repr__(self) -> str:

0 commit comments

Comments
 (0)