Skip to content

Commit 2beee8c

Browse files
committed
fix: repair merge corruption in live_serverless and its tests
live_serverless.py was left syntactically/structurally broken by a bad conflict resolution: an orphaned second docstring, a _live_image property referencing an unimported DEFAULT_PYTHON_VERSION, the two template-injection methods dedented into a dead nested scope, and unreachable code after each validator's return. Reconstructed to the validator-helper design (matches the latest commit's intent): LiveServerlessMixin keeps _create_new_template / _configure_existing_template as real methods that inject dockerArgs, and each subclass validator defers to _apply_default_live_image(data, image_type). The no-op _live_image property and _image_type ClassVars are removed. Also fixes pre-existing breakage the reconstruction surfaced: - test_live_serverless: assert the default image via get_image_name instead of the removed _live_image property - test_live_load_balancer: import DEFAULT_PYTHON_VERSION (local_python_version is not defined in constants on this branch) - test_cpu_disk_sizing: drop a body-less duplicate test (unused-var lint) - test_lb_remote_execution: modernize the scanner fixture to @endpoint and rename it off the test_*.py prefix the scanner skips (was discovering 0)
1 parent cf89281 commit 2beee8c

5 files changed

Lines changed: 69 additions & 97 deletions

File tree

Lines changed: 23 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
# Ship serverless code as you write it. No builds, no deploys -- just run.
2-
from typing import Any, ClassVar
2+
from typing import Any
33

4-
# Ship serverless code as you write it. No builds, no deploys — just run.
54
from pydantic import model_validator
65

7-
from .constants import (
8-
GPU_BASE_IMAGE_PYTHON_VERSION,
9-
get_image_name,
10-
local_python_version,
11-
)
6+
from .constants import DEFAULT_PYTHON_VERSION, get_image_name
127
from .injection import build_injection_cmd
138
from .load_balancer_sls_resource import (
149
CpuLoadBalancerSlsResource,
@@ -22,31 +17,30 @@
2217
class LiveServerlessMixin:
2318
"""Common mixin for live serverless endpoints.
2419
25-
Treats the Flash runtime image as a *default*: if the caller passes an
26-
``imageName`` (e.g. via ``Endpoint(image=...)`` in client mode), that
27-
value wins. Otherwise the Flash runtime image for this resource type is
28-
used so decorator-mode workloads continue to deploy the Flash wrapper.
29-
30-
The default is applied via the ``@model_validator(mode="before")`` on each
31-
concrete subclass (see ``_apply_default_live_image``); reads and writes of
32-
``imageName`` go through the normal Pydantic field machinery so model
33-
serialization, drift detection, and ``setattr`` all stay consistent.
34-
"""Configures process injection via dockerArgs for any base image.
35-
36-
Sets a default base image (user can override via imageName) and generates
37-
dockerArgs to download, extract, and run the flash-worker tarball at container
38-
start time. QB vs LB mode is determined by FLASH_ENDPOINT_TYPE env var at
39-
runtime, not by the Docker image.
20+
Configures process injection via ``dockerArgs`` for any base image, and
21+
treats the Flash runtime image as a *default*: if the caller passes an
22+
``imageName`` (e.g. via ``Endpoint(image=...)`` in client mode), that value
23+
wins. Otherwise the Flash runtime image for this resource type is applied by
24+
the ``@model_validator(mode="before")`` on each concrete subclass (see
25+
``_apply_default_live_image``), so decorator-mode workloads continue to
26+
deploy the Flash wrapper.
27+
28+
The injection ``dockerArgs`` download, extract, and run the flash-worker
29+
tarball at container start; QB vs LB mode is determined by the
30+
``FLASH_ENDPOINT_TYPE`` env var at runtime, not by the Docker image.
4031
"""
4132

42-
_image_type: ClassVar[str] = (
43-
"" # override in subclasses: 'gpu', 'cpu', 'lb', 'lb-cpu'
44-
)
33+
def _create_new_template(self) -> PodTemplate:
34+
"""Create template with dockerArgs for process injection."""
35+
template = super()._create_new_template() # type: ignore[misc]
36+
template.dockerArgs = build_injection_cmd()
37+
return template
4538

46-
@property
47-
def _live_image(self) -> str:
48-
python_version = getattr(self, "python_version", None) or DEFAULT_PYTHON_VERSION
49-
return get_image_name(self._image_type, python_version)
39+
def _configure_existing_template(self) -> None:
40+
"""Configure existing template, adding dockerArgs for injection if not user-set."""
41+
super()._configure_existing_template() # type: ignore[misc]
42+
if self.template is not None and not self.template.dockerArgs: # type: ignore[attr-defined]
43+
self.template.dockerArgs = build_injection_cmd() # type: ignore[attr-defined]
5044

5145

5246
def _apply_default_live_image(data: Any, image_type: str):
@@ -63,18 +57,6 @@ def _apply_default_live_image(data: Any, image_type: str):
6357
data["imageName"] = get_image_name(image_type, python_version)
6458
return data
6559

66-
def _create_new_template(self) -> PodTemplate:
67-
"""Create template with dockerArgs for process injection."""
68-
template = super()._create_new_template() # type: ignore[misc]
69-
template.dockerArgs = build_injection_cmd()
70-
return template
71-
72-
def _configure_existing_template(self) -> None:
73-
"""Configure existing template, adding dockerArgs for injection if not user-set."""
74-
super()._configure_existing_template() # type: ignore[misc]
75-
if self.template is not None and not self.template.dockerArgs: # type: ignore[attr-defined]
76-
self.template.dockerArgs = build_injection_cmd() # type: ignore[attr-defined]
77-
7860

7961
class LiveServerless(LiveServerlessMixin, ServerlessEndpoint):
8062
"""GPU-only live serverless endpoint."""
@@ -84,11 +66,6 @@ class LiveServerless(LiveServerlessMixin, ServerlessEndpoint):
8466
def set_live_serverless_template(cls, data: dict):
8567
"""Default to the GPU Flash runtime image when none is supplied."""
8668
return _apply_default_live_image(data, "gpu")
87-
"""Set default GPU image for Live Serverless."""
88-
if "imageName" not in data:
89-
python_version = data.get("python_version") or GPU_BASE_IMAGE_PYTHON_VERSION
90-
data["imageName"] = get_image_name("gpu", python_version)
91-
return data
9269

9370

9471
class CpuLiveServerless(LiveServerlessMixin, CpuServerlessEndpoint):
@@ -99,11 +76,6 @@ class CpuLiveServerless(LiveServerlessMixin, CpuServerlessEndpoint):
9976
def set_live_serverless_template(cls, data: dict):
10077
"""Default to the CPU Flash runtime image when none is supplied."""
10178
return _apply_default_live_image(data, "cpu")
102-
"""Set default CPU image for Live Serverless."""
103-
if "imageName" not in data:
104-
python_version = data.get("python_version") or local_python_version()
105-
data["imageName"] = get_image_name("cpu", python_version)
106-
return data
10779

10880

10981
class LiveLoadBalancer(LiveServerlessMixin, LoadBalancerSlsResource):
@@ -114,11 +86,6 @@ class LiveLoadBalancer(LiveServerlessMixin, LoadBalancerSlsResource):
11486
def set_live_lb_template(cls, data: dict):
11587
"""Default to the LB Flash runtime image when none is supplied."""
11688
return _apply_default_live_image(data, "lb")
117-
"""Set default image for Live Load-Balanced endpoint."""
118-
if "imageName" not in data:
119-
python_version = data.get("python_version") or GPU_BASE_IMAGE_PYTHON_VERSION
120-
data["imageName"] = get_image_name("lb", python_version)
121-
return data
12289

12390

12491
class CpuLiveLoadBalancer(LiveServerlessMixin, CpuLoadBalancerSlsResource):
@@ -129,8 +96,3 @@ class CpuLiveLoadBalancer(LiveServerlessMixin, CpuLoadBalancerSlsResource):
12996
def set_live_cpu_lb_template(cls, data: dict):
13097
"""Default to the CPU LB Flash runtime image when none is supplied."""
13198
return _apply_default_live_image(data, "lb-cpu")
132-
"""Set default CPU image for Live Load-Balanced endpoint."""
133-
if "imageName" not in data:
134-
python_version = data.get("python_version") or local_python_version()
135-
data["imageName"] = get_image_name("lb-cpu", python_version)
136-
return data

tests/integration/test_cpu_disk_sizing.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,6 @@ def test_mixed_cpu_generations_integration(self):
244244
assert "cpu5c-1-2: max 15GB" in error_msg
245245

246246

247-
class TestLiveServerlessImageIntegration:
248-
"""Test image default + override behavior in live serverless variants (AE-3153)."""
249-
250-
def test_live_serverless_image_consistency(self):
251-
"""LiveServerless variants default to distinct Flash runtime images."""
252-
gpu_live = LiveServerless(name="gpu-live")
253-
cpu_live = CpuLiveServerless(name="cpu-live")
254-
255-
# Verify different default images are used per resource type.
256247
class TestLiveServerlessImageDefaultsIntegration:
257248
"""Test image defaults in live serverless variants."""
258249

tests/integration/test_lb_remote_execution.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ def test_live_load_balancer_image_default_and_override(self):
132132
# Guard against a future regression where both paths collapse to the
133133
# same default (e.g. the override branch reverting to a no-op).
134134
assert default_lb.imageName != custom_lb.imageName
135+
135136
def test_live_load_balancer_default_image(self):
136137
"""Test that LiveLoadBalancer uses GPU LB base image by default."""
137138
lb = LiveLoadBalancer(name="test-api")
@@ -170,50 +171,50 @@ async def qb_func():
170171
assert qb_func.__remote_config__["path"] is None
171172

172173
def test_scanner_discovers_load_balancer_resources(self):
173-
"""Test that scanner can discover LiveLoadBalancer and LoadBalancerSlsResource."""
174+
"""Test that scanner discovers @Endpoint load-balanced route handlers."""
174175
from runpod_flash.cli.commands.build_utils.scanner import RuntimeScanner
175176
from pathlib import Path
176177
import tempfile
177178

178-
# Create temporary Python file with LoadBalancer resource
179+
# Uses the current @Endpoint API (remote is deprecated). The worker file
180+
# must NOT be named test_*.py — the scanner skips test files, so a
181+
# test-prefixed fixture would yield zero discovered functions.
179182
code = """
180-
from runpod_flash import LiveLoadBalancer, LoadBalancerSlsResource, remote
183+
from runpod_flash.endpoint import Endpoint
184+
from runpod_flash.core.resources.gpu import GpuGroup
181185
182-
# Test LiveLoadBalancer discovery
183-
api = LiveLoadBalancer(name="test-api")
186+
api = Endpoint(name="test-api", gpu=GpuGroup.AMPERE_16)
184187
185-
@remote(api, method="POST", path="/api/process")
188+
@api.post("/api/process")
186189
async def process_data(x: int):
187190
return {"result": x}
188191
189-
# Test LoadBalancerSlsResource discovery
190-
deployed = LoadBalancerSlsResource(name="deployed-api", imageName="test:latest")
192+
status_api = Endpoint(name="deployed-api", gpu=GpuGroup.AMPERE_16)
191193
192-
@remote(deployed, method="GET", path="/api/status")
194+
@status_api.get("/api/status")
193195
def get_status():
194196
return {"status": "ok"}
195197
"""
196198

197199
with tempfile.TemporaryDirectory() as tmpdir:
198200
project_dir = Path(tmpdir)
199-
py_file = project_dir / "test_api.py"
201+
py_file = project_dir / "worker_api.py"
200202
py_file.write_text(code)
201203

202204
scanner = RuntimeScanner(project_dir)
203205
functions = scanner.discover_remote_functions()
204206

205-
# Verify both resources were discovered
207+
# Both LB route handlers are discovered.
206208
assert len(functions) == 2
207209

208-
# Verify resource types are correctly identified
209-
resource_types = {f.resource_type for f in functions}
210-
assert "LiveLoadBalancer" in resource_types
211-
assert "LoadBalancerSlsResource" in resource_types
212-
213-
# Verify resource configs were extracted
214-
assert "test-api-fb" in scanner.resource_types
215-
assert scanner.resource_types["test-api-fb"] == "LiveLoadBalancer"
216-
assert "deployed-api-fb" in scanner.resource_types
217-
assert (
218-
scanner.resource_types["deployed-api-fb"] == "LoadBalancerSlsResource"
219-
)
210+
# Endpoint(gpu=...) with route handlers resolves to LiveLoadBalancer.
211+
assert {f.resource_type for f in functions} == {"LiveLoadBalancer"}
212+
assert all(f.is_load_balanced for f in functions)
213+
214+
# HTTP method and path are captured per route.
215+
routes = {(f.http_method, f.http_path) for f in functions}
216+
assert routes == {("POST", "/api/process"), ("GET", "/api/status")}
217+
218+
# Resource configs are tracked by name.
219+
assert scanner.resource_types["test-api"] == "LiveLoadBalancer"
220+
assert scanner.resource_types["deployed-api"] == "LiveLoadBalancer"

tests/unit/resources/test_live_load_balancer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
import pytest
77
from runpod_flash.core.resources.constants import (
8+
DEFAULT_PYTHON_VERSION,
89
GPU_BASE_IMAGE_PYTHON_VERSION,
9-
local_python_version,
1010
)
1111
from runpod_flash.core.resources.cpu import CpuInstanceType
1212
from runpod_flash.core.resources.live_serverless import (
@@ -202,7 +202,7 @@ def test_cpu_live_load_balancer_default_image_tag(self):
202202
os.environ.pop("FLASH_IMAGE_TAG", None)
203203

204204
lb = CpuLiveLoadBalancer(name="test-lb")
205-
assert f"py{local_python_version()}" in lb.imageName
205+
assert f"py{DEFAULT_PYTHON_VERSION}" in lb.imageName
206206
assert lb.template is not None
207207
assert lb.template.imageName == lb.imageName
208208

tests/unit/resources/test_live_serverless.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def test_live_serverless_image_default_unchanged(self):
5353
"""LiveServerless still defaults to the Flash GPU runtime image."""
5454
live_serverless = LiveServerless(name="example_gpu_live_serverless")
5555
assert "flash:" in live_serverless.imageName
56+
5657
def test_live_serverless_user_can_override_image(self):
5758
"""Test user can set custom imageName (BYOI)."""
5859
live_serverless = LiveServerless(
@@ -128,6 +129,7 @@ def test_cpu_live_serverless_image_default_unchanged(self):
128129
instanceIds=[CpuInstanceType.CPU3G_1_4],
129130
)
130131
assert "flash-cpu:" in live_serverless.imageName
132+
131133
def test_cpu_live_serverless_user_can_override_image(self):
132134
"""Test CpuLiveServerless allows user to set custom image."""
133135
live_serverless = CpuLiveServerless(name="test", imageName="python:3.11-slim")
@@ -200,13 +202,28 @@ def test_docker_args_set_on_existing_template(self):
200202

201203
def test_image_name_property_gpu(self):
202204
"""LiveServerless defaults imageName to the Flash runtime image when none supplied."""
205+
from runpod_flash.core.resources.constants import (
206+
DEFAULT_PYTHON_VERSION,
207+
get_image_name,
208+
)
209+
203210
live_serverless = LiveServerless(name="test")
204-
assert live_serverless.imageName == live_serverless._live_image
211+
assert live_serverless.imageName == get_image_name(
212+
"gpu", DEFAULT_PYTHON_VERSION
213+
)
205214

206215
def test_image_name_property_cpu(self):
207216
"""CpuLiveServerless defaults imageName to the Flash runtime image when none supplied."""
217+
from runpod_flash.core.resources.constants import (
218+
DEFAULT_PYTHON_VERSION,
219+
get_image_name,
220+
)
221+
208222
live_serverless = CpuLiveServerless(name="test")
209-
assert live_serverless.imageName == live_serverless._live_image
223+
assert live_serverless.imageName == get_image_name(
224+
"cpu", DEFAULT_PYTHON_VERSION
225+
)
226+
210227
def test_all_live_classes_have_docker_args(self):
211228
"""Test all Live* classes set dockerArgs on their templates."""
212229
classes_and_kwargs = [
@@ -262,6 +279,7 @@ def test_default_image_validator_passes_through_non_dict(self):
262279
original = LiveServerless(name="test", imageName="byo/image:v1")
263280
revalidated = LiveServerless.model_validate(original)
264281
assert revalidated.imageName == "byo/image:v1"
282+
265283
def test_live_serverless_byoi_gpu(self):
266284
"""Test LiveServerless respects user-provided imageName."""
267285
live_serverless = LiveServerless(name="test", imageName="custom/gpu:v1")

0 commit comments

Comments
 (0)