|
7 | 7 | raises ``SystemExit`` and then blocks in interpreter shutdown joining those |
8 | 8 | threads, so the worker logs "unhealthy, exiting." but never terminates. |
9 | 9 | The exit must go through ``os._exit`` to terminate unconditionally. |
| 10 | +
|
| 11 | +Also covers SLS-380: the best-effort unhealthy report the worker sends to the |
| 12 | +host before force-exiting. |
10 | 13 | """ |
11 | 14 |
|
12 | 15 | import subprocess |
13 | 16 | import sys |
14 | | -from unittest.mock import patch |
| 17 | +from unittest.mock import MagicMock, patch |
| 18 | + |
| 19 | +import pytest |
15 | 20 |
|
16 | 21 | from runpod.serverless.modules import rp_fitness |
17 | 22 |
|
@@ -92,3 +97,78 @@ def test_terminate_unhealthy_exits_even_if_flush_raises(): |
92 | 97 | rp_fitness._terminate_unhealthy(1) |
93 | 98 |
|
94 | 99 | mock_exit.assert_called_once_with(1) |
| 100 | + |
| 101 | + |
| 102 | +def test_report_unhealthy_posts_check_and_reason(monkeypatch): |
| 103 | + monkeypatch.setenv("RUNPOD_WEBHOOK_PING", "https://api.test/ping/$RUNPOD_POD_ID") |
| 104 | + monkeypatch.setenv("RUNPOD_AI_API_KEY", "key-123") |
| 105 | + |
| 106 | + fake_session = MagicMock() |
| 107 | + with patch("runpod.http_client.SyncClientSession", return_value=fake_session), \ |
| 108 | + patch("runpod.serverless.modules.worker_state.WORKER_ID", "podABC"): |
| 109 | + rp_fitness._report_unhealthy("_cuda_init_check", "RuntimeError: boom") |
| 110 | + |
| 111 | + assert fake_session.get.call_count == 1 |
| 112 | + call = fake_session.get.call_args |
| 113 | + url = call.args[0] if call.args else call.kwargs["url"] |
| 114 | + params = call.kwargs["params"] |
| 115 | + assert "podABC" in url # $RUNPOD_POD_ID substituted |
| 116 | + assert params["status"] == "unhealthy" |
| 117 | + assert params["check"] == "_cuda_init_check" |
| 118 | + assert params["reason"] == "RuntimeError: boom" |
| 119 | + fake_session.headers.update.assert_called_once_with({"Authorization": "key-123"}) |
| 120 | + |
| 121 | + |
| 122 | +def test_report_unhealthy_skipped_without_ping_url(monkeypatch): |
| 123 | + monkeypatch.delenv("RUNPOD_WEBHOOK_PING", raising=False) |
| 124 | + monkeypatch.setenv("RUNPOD_AI_API_KEY", "key-123") |
| 125 | + with patch("runpod.http_client.SyncClientSession") as session_cls: |
| 126 | + rp_fitness._report_unhealthy("_memory_check", "RuntimeError: low") |
| 127 | + session_cls.assert_not_called() |
| 128 | + |
| 129 | + |
| 130 | +def test_report_unhealthy_truncates_long_reason(monkeypatch): |
| 131 | + monkeypatch.setenv("RUNPOD_WEBHOOK_PING", "https://api.test/ping") |
| 132 | + monkeypatch.setenv("RUNPOD_AI_API_KEY", "key-123") |
| 133 | + |
| 134 | + fake_session = MagicMock() |
| 135 | + with patch("runpod.http_client.SyncClientSession", return_value=fake_session): |
| 136 | + rp_fitness._report_unhealthy("_disk_check", "x" * 300) |
| 137 | + |
| 138 | + params = fake_session.get.call_args.kwargs["params"] |
| 139 | + assert len(params["reason"]) == 256 |
| 140 | + |
| 141 | + |
| 142 | +def test_report_unhealthy_skipped_without_api_key(monkeypatch): |
| 143 | + monkeypatch.setenv("RUNPOD_WEBHOOK_PING", "https://api.test/ping") |
| 144 | + monkeypatch.delenv("RUNPOD_AI_API_KEY", raising=False) |
| 145 | + with patch("runpod.http_client.SyncClientSession") as session_cls: |
| 146 | + rp_fitness._report_unhealthy("_memory_check", "RuntimeError: low") |
| 147 | + session_cls.assert_not_called() |
| 148 | + |
| 149 | + |
| 150 | +def test_report_unhealthy_swallows_errors(monkeypatch): |
| 151 | + monkeypatch.setenv("RUNPOD_WEBHOOK_PING", "https://api.test/ping") |
| 152 | + monkeypatch.setenv("RUNPOD_AI_API_KEY", "key-123") |
| 153 | + fake_session = MagicMock() |
| 154 | + fake_session.get.side_effect = RuntimeError("network down") |
| 155 | + with patch("runpod.http_client.SyncClientSession", return_value=fake_session): |
| 156 | + rp_fitness._report_unhealthy("_disk_check", "RuntimeError: full") # must not raise |
| 157 | + |
| 158 | + |
| 159 | +@pytest.mark.asyncio |
| 160 | +async def test_failure_reports_before_exit(): |
| 161 | + from runpod.serverless.modules.rp_fitness import register_fitness_check, run_fitness_checks |
| 162 | + |
| 163 | + @register_fitness_check |
| 164 | + def _cuda_init_check(): |
| 165 | + raise RuntimeError("device busy") |
| 166 | + |
| 167 | + with patch("runpod.serverless.modules.rp_fitness._report_unhealthy") as mock_report: |
| 168 | + with pytest.raises(SystemExit): |
| 169 | + await run_fitness_checks() |
| 170 | + |
| 171 | + mock_report.assert_called_once() |
| 172 | + args = mock_report.call_args.args |
| 173 | + assert args[0] == "_cuda_init_check" |
| 174 | + assert "RuntimeError" in args[1] and "device busy" in args[1] |
0 commit comments