#9336 Python: Translate closed-channel errors and stop downgrading RipsError

After the heartbeat closes a dead-server channel, gRPC raises
ValueError("Cannot invoke RPC on closed channel!") rather than a
grpc.RpcError, which slipped past the existing pdmobject wrappers and
surfaced to the user as a bare ValueError nested in a traceback.

- pdmobject add_method decorator and _call_pdm_method_void /
  _return_value / _return_optional_value now also catch the
  closed-channel ValueError and translate it to RipsError.
- well_path.add_well_log no longer rewraps every exception as
  RuntimeError. Cleanup of temporary key-value entries still runs, but
  the original typed exception (e.g. RipsError with code/details) is
  re-raised so callers can inspect it.
This commit is contained in:
Kristian Bendiksen
2026-05-04 20:03:39 +02:00
parent f8d180f48d
commit 85fc0220c7
2 changed files with 41 additions and 3 deletions
+36
View File
@@ -36,6 +36,14 @@ F = TypeVar("F", bound=Callable[..., Any])
C = TypeVar("C")
def _is_closed_channel_error(exc: BaseException) -> bool:
# gRPC raises ValueError("Cannot invoke RPC on closed channel!") when an
# RPC is attempted after the channel has been closed (e.g. by the
# heartbeat after detecting a dead server). Translate to RipsError so
# callers see a typed exception instead of a bare ValueError.
return isinstance(exc, ValueError) and "closed channel" in str(exc)
def add_method(cls: C) -> Callable[[F], F]:
def decorator(func: F) -> F:
def wrapper(*args, **kwargs):
@@ -45,6 +53,13 @@ def add_method(cls: C) -> Callable[[F], F]:
raise RipsError(
e.details(), code=e.code(), details=e.details()
) from None
except ValueError as e:
if _is_closed_channel_error(e):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from e
raise
# Explicitly preserve signature for Sphinx documentation
wrapper.__name__ = func.__name__
@@ -563,6 +578,13 @@ class PdmObjectBase:
raise RipsError(
"%s" % exc.details(), code=exc.code(), details=exc.details()
) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
def _call_pdm_method_return_value(
self, method_name: str, class_definition: Type[PdmObjectT], **kwargs: Any
@@ -584,6 +606,13 @@ class PdmObjectBase:
raise RipsError(
"%s" % exc.details(), code=exc.code(), details=exc.details()
) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
def _call_pdm_method_return_optional_value(
self, method_name: str, class_definition: Type[PdmObjectT], **kwargs: Any
@@ -616,6 +645,13 @@ class PdmObjectBase:
raise RipsError(
"%s" % exc.details(), code=exc.code(), details=exc.details()
) from None
except ValueError as exc:
if _is_closed_channel_error(exc):
raise RipsError(
"ResInsight gRPC channel is closed "
"(server may have crashed or been shut down)"
) from exc
raise
def update(self) -> None:
"""Sync all fields from the Python Object to ResInsight
+5 -3
View File
@@ -224,14 +224,16 @@ def add_well_log(
tvd_rkb_key=tvd_rkb_key,
)
return well_log
except Exception as e:
# Clean up all temporary keys on failure
except Exception:
# Clean up all temporary keys on failure, then re-raise the
# original exception so callers see the typed RipsError (with
# gRPC code/details) instead of a generic RuntimeError.
for temp_key in temp_keys:
try:
project.remove_key_values(temp_key)
except Exception:
pass # Ignore cleanup errors
raise RuntimeError(f"Failed to create well log: {str(e)}") from e
raise
@add_method(WellPath)