Skip to content

Commit c63e369

Browse files
committed
docs(deployments): trim restate-y comments per review
Address review feedback on the openshell backend: reformat the readiness fail-closed rationale as a bulleted exit-code table, and cut the _exec_detached docstring's narration down to the non-obvious facts (the None exit-code semantics and the timeout rationale) instead of restating what the code does. Also satisfy ty in the openshell readiness tests: construct Probe via pydantic aliases (httpGet/tcpSocket), since ty synthesizes __init__ from aliases and does not honor populate_by_name, and route _readiness_probe_command unpacks through a helper that narrows its tuple|None return. Test-only; no behavior change. Rename test_openshell_backend_mocked.py -> test_backend.py to match the k8s unit backend test; the unit/ vs integration/ tree already conveys mocked vs live, so the _mocked suffix was redundant. Signed-off-by: Max Dubrinsky <mdubrinsky@nvidia.com>
1 parent 58e96be commit c63e369

2 files changed

Lines changed: 32 additions & 24 deletions

File tree

plugins/nemo-deployments/src/nemo_deployments_plugin/backends/openshell/backend.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -660,10 +660,11 @@ async def _try_get_sandbox(self, sandbox_nm: str) -> Any | None:
660660
async def _exec_detached(
661661
self, sandbox_id: str, command: list[str], *, timeout: int | None = None
662662
) -> tuple[int | None, str]:
663-
"""Run a command, draining its event stream. Returns (exit_code, combined output).
663+
"""Run *command* to completion, returning (exit_code, stdout+stderr merged);
664+
``exit_code`` is None when the stream carried no exit event.
664665
665-
*timeout* bounds both the RPC and the sandbox-side command; it defaults to the
666-
executor's control-plane ``request_timeout_seconds``. Readiness probes pass a much
666+
*timeout* bounds both the RPC and the sandbox-side command, defaulting to the
667+
executor's control-plane ``request_timeout_seconds``; readiness probes pass a much
667668
shorter bound so a hung probe cannot stall the serial reconcile loop.
668669
"""
669670
timeout = timeout if timeout is not None else self._executor_config.request_timeout_seconds
@@ -698,14 +699,14 @@ async def _readiness_pending(self, sandbox_id: str, container: Container) -> Bac
698699
return None
699700
command, description, exec_timeout = probe_command
700701
exit_code, _ = await self._exec_detached(sandbox_id, command, timeout=exec_timeout)
701-
# Readiness fails closed, unlike liveness. Liveness fails open on an undecidable
702-
# probe so a flaky RPC never demotes a healthy deployment, but readiness gates
703-
# admission the other way: only a probe that positively proves reachability
704-
# (exit 0) may expose the port and flip to sticky READY. A rare no-exit-event
705-
# (None) stays STARTING and self-heals -- the port is not exposed while pending,
706-
# so the next poll re-probes; a workload that can never be probed never claims
707-
# READY, which is the contract this gate exists to keep. Timeouts and RPC errors
708-
# surface as RpcError -> UNKNOWN upstream, not as a None exit here.
702+
# Readiness fails closed (liveness fails open): the gate admits only positive
703+
# proof of reachability, so a flaky probe never exposes an unready workload.
704+
# - exit 0 -> reachable; expose the port and read READY
705+
# - nonzero -> not reachable yet; stay STARTING
706+
# - no exit event -> undecidable; stay STARTING and re-probe next poll (the port
707+
# is not exposed while pending, so this self-heals)
708+
# A workload that can never be probed never claims READY -- the contract this gate
709+
# keeps. Timeouts and RPC errors surface as UNKNOWN upstream, not as a None exit.
709710
if exit_code == 0:
710711
return None
711712
return BackendStatusUpdate(status="STARTING", status_message=f"Awaiting readiness: {description}")

plugins/nemo-deployments/tests/unit/backends/openshell/test_openshell_backend_mocked.py renamed to plugins/nemo-deployments/tests/unit/backends/openshell/test_backend.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ async def test_read_status_gates_on_declared_httpget_probe(
507507
) -> None:
508508
# A declared httpGet readinessProbe is honoured against loopback; a failing probe
509509
# keeps the deployment STARTING and unexposed, and the probe hits the declared path.
510-
mock_entities.get.return_value = _config_with_readiness(Probe(http_get=HTTPGetAction(path="/health", port=8000)))
510+
mock_entities.get.return_value = _config_with_readiness(Probe(httpGet=HTTPGetAction(path="/health", port=8000)))
511511
mock_stub.GetSandbox.return_value = _sandbox(pb.SANDBOX_PHASE_READY)
512512
mock_stub.ExecSandbox.side_effect = [
513513
_exec_events(0), # marker present
@@ -677,8 +677,15 @@ async def test_read_status_cleanup_swallows_delete_error(
677677
mock_stub.DeleteSandbox.assert_called_once()
678678

679679

680+
def _require_probe_command(container: Container) -> tuple[list[str], str, int]:
681+
"""Return the container's readiness probe command, asserting it exists (narrows None)."""
682+
probe_command = _readiness_probe_command(container)
683+
assert probe_command is not None
684+
return probe_command
685+
686+
680687
def test_readiness_probe_command_defaults_to_tcp_on_the_first_port() -> None:
681-
command, description, timeout = _readiness_probe_command(_config().containers[0])
688+
command, description, timeout = _require_probe_command(_config().containers[0])
682689
assert command[:2] == ["/bin/sh", "-c"]
683690
assert "127.0.0.1" in command[2]
684691
assert description == "tcp 127.0.0.1:8000"
@@ -687,16 +694,16 @@ def test_readiness_probe_command_defaults_to_tcp_on_the_first_port() -> None:
687694

688695

689696
def test_readiness_probe_command_uses_declared_httpget() -> None:
690-
container = _config_with_readiness(Probe(http_get=HTTPGetAction(path="/ready", port=8000))).containers[0]
691-
command, description, _timeout = _readiness_probe_command(container)
697+
container = _config_with_readiness(Probe(httpGet=HTTPGetAction(path="/ready", port=8000))).containers[0]
698+
command, description, _timeout = _require_probe_command(container)
692699
assert "http://127.0.0.1:8000/ready" in command[2]
693700
assert description == "httpGet http://127.0.0.1:8000/ready"
694701

695702

696703
def test_readiness_probe_command_runs_declared_exec_directly() -> None:
697704
probe = Probe(exec=ExecAction(command=["/bin/true"]), timeoutSeconds=4)
698705
container = _config_with_readiness(probe).containers[0]
699-
command, description, timeout = _readiness_probe_command(container)
706+
command, description, timeout = _require_probe_command(container)
700707
assert command == ["/bin/true"]
701708
assert description == "exec readiness probe"
702709
# An exec probe is bounded by its own timeoutSeconds, not the control-plane deadline,
@@ -705,8 +712,8 @@ def test_readiness_probe_command_runs_declared_exec_directly() -> None:
705712

706713

707714
def test_readiness_probe_command_resolves_named_tcp_socket_port() -> None:
708-
container = _config_with_readiness(Probe(tcp_socket=TCPSocketAction(port="http"))).containers[0]
709-
_command, description, _timeout = _readiness_probe_command(container)
715+
container = _config_with_readiness(Probe(tcpSocket=TCPSocketAction(port="http"))).containers[0]
716+
_command, description, _timeout = _require_probe_command(container)
710717
assert description == "tcp 127.0.0.1:8000"
711718

712719

@@ -716,17 +723,17 @@ def test_readiness_probe_command_is_none_without_probe_or_ports() -> None:
716723

717724
def test_readiness_probe_command_https_uses_unverified_context() -> None:
718725
container = _config_with_readiness(
719-
Probe(http_get=HTTPGetAction(path="/health", port=8000, scheme="HTTPS"))
726+
Probe(httpGet=HTTPGetAction(path="/health", port=8000, scheme="HTTPS"))
720727
).containers[0]
721-
command, _description, _timeout = _readiness_probe_command(container)
728+
command, _description, _timeout = _require_probe_command(container)
722729
assert "https://127.0.0.1:8000/health" in command[2]
723730
# An https probe against a loopback/self-signed cert must not fail verification.
724731
assert "_create_unverified_context" in command[2]
725732

726733

727734
def test_readiness_probe_command_normalizes_httpget_path_without_leading_slash() -> None:
728-
container = _config_with_readiness(Probe(http_get=HTTPGetAction(path="ready", port=8000))).containers[0]
729-
_command, description, _timeout = _readiness_probe_command(container)
735+
container = _config_with_readiness(Probe(httpGet=HTTPGetAction(path="ready", port=8000))).containers[0]
736+
_command, description, _timeout = _require_probe_command(container)
730737
assert description == "httpGet http://127.0.0.1:8000/ready"
731738

732739

@@ -743,10 +750,10 @@ def test_readiness_probe_command_skips_udp_only_ports() -> None:
743750
def test_readiness_probe_command_falls_back_when_declared_port_unresolvable() -> None:
744751
# A declared probe naming a port absent from the container falls back to the first
745752
# TCP port rather than skipping the gate (which would re-open the bind race).
746-
container = _config_with_readiness(Probe(http_get=HTTPGetAction(path="/health", port="does-not-exist"))).containers[
753+
container = _config_with_readiness(Probe(httpGet=HTTPGetAction(path="/health", port="does-not-exist"))).containers[
747754
0
748755
]
749-
_command, description, _timeout = _readiness_probe_command(container)
756+
_command, description, _timeout = _require_probe_command(container)
750757
assert description == "httpGet http://127.0.0.1:8000/health"
751758

752759

0 commit comments

Comments
 (0)