Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/runpod_flash/core/resources/serverless.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
# Prefix applied to endpoint names during live provisioning
LIVE_PREFIX = "live-"

# Default client-side HTTP timeout for runsync calls (seconds)
DEFAULT_RUNSYNC_TIMEOUT_S = 60


log = logging.getLogger(__name__)

Expand Down Expand Up @@ -1201,10 +1204,16 @@ async def runsync(self, payload: Dict[str, Any]) -> "JobOutput":
if not self.id:
raise ValueError("Serverless is not deployed")

timeout_s: float = (
self.executionTimeoutMs / 1000
if self.executionTimeoutMs is not None and self.executionTimeoutMs > 0
else DEFAULT_RUNSYNC_TIMEOUT_S
)

def _fetch_job():
log.info(f"{self} | API /runsync")
return self.endpoint.rp_client.post(
f"{self.id}/runsync", payload, timeout=60
f"{self.id}/runsync", payload, timeout=timeout_s
)

try:
Expand Down
115 changes: 115 additions & 0 deletions tests/unit/resources/test_serverless.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,6 +1272,8 @@ def test_serverless_endpoint_with_existing_template(self):

def test_serverless_endpoint_template_env_override(self):
"""Test ServerlessEndpoint overrides template env vars."""
from runpod_flash.core.resources.template import PodTemplate, KeyValuePair

template = PodTemplate(
name="existing-template",
imageName="test/image:v1",
Expand Down Expand Up @@ -2567,3 +2569,116 @@ async def test_update_echoes_empty_env_for_default_template_env(self):
template_payload = mock_client.update_template.call_args.args[0]
env_entries = template_payload.get("env", [])
assert env_entries == []


class TestServerlessRunsyncTimeout:
"""Test that runsync uses executionTimeoutMs as client timeout."""

Comment on lines +2574 to 2576
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff removes a large set of existing unit tests (e.g. for _inject_template_env, _build_template_update_payload, and update() env preservation/injection behavior) and I can't find equivalent coverage elsewhere in tests/. Since the corresponding production code still exists in serverless.py, this looks like an unintended deletion that significantly reduces regression coverage. Please restore these tests (or move them to a new test module) and keep this PR focused on the runsync-timeout change, or explicitly document/justify the removal if it’s intentional.

Copilot uses AI. Check for mistakes.
@pytest.fixture
def resource_with_timeout(self):
"""ServerlessResource with executionTimeoutMs set to 300s."""
resource = ServerlessResource(
name="test-gpu-worker",
executionTimeoutMs=300_000,
)
resource.id = "ep-abc123"
return resource

@pytest.fixture
def resource_default_timeout(self):
"""ServerlessResource with default executionTimeoutMs (0)."""
resource = ServerlessResource(
name="test-default",
executionTimeoutMs=0,
)
resource.id = "ep-default"
return resource

@pytest.mark.asyncio
async def test_runsync_uses_execution_timeout(self, resource_with_timeout):
"""runsync should use executionTimeoutMs/1000 as client timeout."""
mock_rp_client = MagicMock()
mock_rp_client.post.return_value = {
"id": "job-1",
"workerId": "w-1",
"status": "COMPLETED",
"delayTime": 100,
"executionTime": 5000,
"output": {"result": "ok"},
}

mock_endpoint = MagicMock()
mock_endpoint.rp_client = mock_rp_client

with patch.object(
ServerlessResource,
"endpoint",
new_callable=lambda: property(lambda self: mock_endpoint),
):
result = await resource_with_timeout.runsync({"input": "data"})

# The client timeout should be executionTimeoutMs / 1000 = 300s, not 60s
mock_rp_client.post.assert_called_once_with(
"ep-abc123/runsync", {"input": "data"}, timeout=300
)
assert result.status == "COMPLETED"

@pytest.mark.asyncio
async def test_runsync_default_timeout_when_zero(self, resource_default_timeout):
"""runsync should fall back to 60s when executionTimeoutMs is 0."""
mock_rp_client = MagicMock()
mock_rp_client.post.return_value = {
"id": "job-2",
"workerId": "w-2",
"status": "COMPLETED",
"delayTime": 50,
"executionTime": 2000,
"output": None,
}

mock_endpoint = MagicMock()
mock_endpoint.rp_client = mock_rp_client

with patch.object(
ServerlessResource,
"endpoint",
new_callable=lambda: property(lambda self: mock_endpoint),
):
await resource_default_timeout.runsync({"input": "data"})

mock_rp_client.post.assert_called_once_with(
"ep-default/runsync", {"input": "data"}, timeout=60
)

@pytest.mark.asyncio
async def test_runsync_default_timeout_when_none(self):
"""runsync should fall back to 60s when executionTimeoutMs is None."""
resource = ServerlessResource(
name="test-none",
executionTimeoutMs=None,
)
resource.id = "ep-none"

mock_rp_client = MagicMock()
mock_rp_client.post.return_value = {
"id": "job-3",
"workerId": "w-3",
"status": "COMPLETED",
"delayTime": 10,
"executionTime": 1000,
"output": None,
}

mock_endpoint = MagicMock()
mock_endpoint.rp_client = mock_rp_client

with patch.object(
ServerlessResource,
"endpoint",
new_callable=lambda: property(lambda self: mock_endpoint),
):
await resource.runsync({"input": "data"})

mock_rp_client.post.assert_called_once_with(
"ep-none/runsync", {"input": "data"}, timeout=60
)
Loading
Loading