From 643e661f44f886e7f66c06ff212902e1d67ed451 Mon Sep 17 00:00:00 2001 From: Jvst Me Date: Mon, 17 Aug 2026 20:53:18 +0200 Subject: [PATCH] Add gateway assignment events - `Service assigned to gateway` - `Service assigned to run without a gateway` These new events provide a way to see which gateway is supposed to serve the service, even before the service is actually registered there. --- .../server/services/services/__init__.py | 19 +++++++++++++++-- .../_internal/server/routers/test_runs.py | 7 +++++++ .../server/services/services/test_services.py | 21 ------------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/dstack/_internal/server/services/services/__init__.py b/src/dstack/_internal/server/services/services/__init__.py index 4421b6055..b5e03a8be 100644 --- a/src/dstack/_internal/server/services/services/__init__.py +++ b/src/dstack/_internal/server/services/services/__init__.py @@ -19,6 +19,7 @@ from dstack._internal.core.models.services import OpenAIChatModel from dstack._internal.server import settings from dstack._internal.server.models import GatewayModel, RunModel +from dstack._internal.server.services import events from dstack._internal.server.services.gateways import ( get_gateway_compute_models, get_gateway_configuration, @@ -73,7 +74,7 @@ async def register_service(session: AsyncSession, run_model: RunModel, run_spec: service_spec = await _register_service_in_gateway(session, run_model, run_spec, gateway) run_model.gateway = gateway elif not settings.FORBID_SERVICES_WITHOUT_GATEWAY: - service_spec = _register_service_in_server(run_model, run_spec) + service_spec = _register_service_in_server(session, run_model, run_spec) else: raise ResourceNotExistsError( "This dstack-server installation forbids services without a gateway." @@ -137,10 +138,18 @@ async def _register_service_in_gateway( service_url=service_url, model_url=model_url, ) + events.emit( + session, + "Service assigned to gateway", + actor=events.SystemActor(), + targets=[events.Target.from_model(run_model), events.Target.from_model(gateway)], + ) return service_spec -def _register_service_in_server(run_model: RunModel, run_spec: RunSpec) -> ServiceSpec: +def _register_service_in_server( + session: AsyncSession, run_model: RunModel, run_spec: RunSpec +) -> ServiceSpec: assert run_spec.configuration.type == "service" if run_spec.configuration.https not in ( None, @@ -170,6 +179,12 @@ def _register_service_in_server(run_model: RunModel, run_spec: RunSpec) -> Servi model_url = service_url.rstrip("/") + run_spec.configuration.model.prefix else: model_url = f"/proxy/models/{run_model.project.name}/" + events.emit( + session, + "Service assigned to run without a gateway", + actor=events.SystemActor(), + targets=[events.Target.from_model(run_model)], + ) return _get_service_spec( configuration=run_spec.configuration, service_url=service_url, diff --git a/src/tests/_internal/server/routers/test_runs.py b/src/tests/_internal/server/routers/test_runs.py index cc83038ff..60d958f6a 100644 --- a/src/tests/_internal/server/routers/test_runs.py +++ b/src/tests/_internal/server/routers/test_runs.py @@ -4005,6 +4005,13 @@ async def test_submit_to_correct_proxy( res = await session.execute(select(RunModel)) run = res.scalar_one() assert (run.gateway_id is not None) == is_gateway + event_messages = {e.message for e in await list_events(session)} + if is_gateway: + assert "Service assigned to gateway" in event_messages + assert "Service assigned to run without a gateway" not in event_messages + else: + assert "Service assigned to gateway" not in event_messages + assert "Service assigned to run without a gateway" in event_messages @pytest.mark.asyncio @pytest.mark.parametrize("populate_configuration", [True, False]) diff --git a/src/tests/_internal/server/services/services/test_services.py b/src/tests/_internal/server/services/services/test_services.py index f65fdbdf8..27854893b 100644 --- a/src/tests/_internal/server/services/services/test_services.py +++ b/src/tests/_internal/server/services/services/test_services.py @@ -1,9 +1,6 @@ from typing import Literal, Optional, Union from unittest.mock import MagicMock -import pytest - -from dstack._internal.core.errors import ServerClientError from dstack._internal.core.models.backends.base import BackendType from dstack._internal.core.models.configurations import ServiceConfiguration from dstack._internal.core.models.gateways import ( @@ -14,7 +11,6 @@ ) from dstack._internal.core.models.runs import RunSpec from dstack._internal.server.services.services import ( - _register_service_in_server, _should_show_service_https, should_configure_service_https_on_gateway, ) @@ -118,20 +114,3 @@ def test_false_disables_https_regardless_of_gateway_certificate(self) -> None: run_spec = _service_run_spec(https=False) gw = _gateway_config(certificate=LetsEncryptGatewayCertificate()) assert _should_show_service_https(run_spec, gw) is False - - -class TestRegisterServiceInServerHttps: - def test_allows_default_true_without_gateway(self) -> None: - run_spec = _service_run_spec(https=True) - result = _register_service_in_server(_mock_run_model(), run_spec) - assert result is not None - - def test_allows_auto_without_gateway(self) -> None: - run_spec = _service_run_spec(https="auto") - result = _register_service_in_server(_mock_run_model(), run_spec) - assert result is not None - - def test_rejects_explicit_false_without_gateway(self) -> None: - run_spec = _service_run_spec(https=False) - with pytest.raises(ServerClientError, match="not allowed without a gateway"): - _register_service_in_server(_mock_run_model(), run_spec)