From 39c3f6fb7b34f426b19e249fa034dc690fbabbdc Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 11 Jul 2025 11:53:38 +0500 Subject: [PATCH] Fix 500 errors when requesting file logs --- .../_internal/server/services/logs/filelog.py | 17 ++++---- .../_internal/server/services/test_logs.py | 42 +++---------------- 2 files changed, 13 insertions(+), 46 deletions(-) diff --git a/src/dstack/_internal/server/services/logs/filelog.py b/src/dstack/_internal/server/services/logs/filelog.py index 905ee3527b..b1c4ce2bc0 100644 --- a/src/dstack/_internal/server/services/logs/filelog.py +++ b/src/dstack/_internal/server/services/logs/filelog.py @@ -2,6 +2,7 @@ from typing import List, Union from uuid import UUID +from dstack._internal.core.errors import ServerClientError from dstack._internal.core.models.logs import ( JobSubmissionLogs, LogEvent, @@ -14,7 +15,6 @@ from dstack._internal.server.schemas.runner import LogEvent as RunnerLogEvent from dstack._internal.server.services.logs.base import ( LogStorage, - LogStorageError, b64encode_raw_message, unix_time_ms_to_datetime, ) @@ -30,9 +30,6 @@ def __init__(self, root: Union[Path, str, None] = None) -> None: self.root = Path(root) def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmissionLogs: - if request.descending: - raise LogStorageError("descending: true is not supported") - log_producer = LogProducer.RUNNER if request.diagnose else LogProducer.JOB log_file_path = self._get_log_file_path( project_name=project.name, @@ -46,11 +43,11 @@ def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmi try: start_line = int(request.next_token) if start_line < 0: - raise LogStorageError( + raise ServerClientError( f"Invalid next_token: {request.next_token}. Must be a non-negative integer." ) except ValueError: - raise LogStorageError( + raise ServerClientError( f"Invalid next_token: {request.next_token}. Must be a valid integer." ) @@ -59,9 +56,12 @@ def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmi current_line = 0 try: + # FIXME: Do not read all the lines in memory with open(log_file_path) as f: lines = f.readlines() - + except FileNotFoundError: + pass + else: for i, line in enumerate(lines): if current_line < start_line: current_line += 1 @@ -83,9 +83,6 @@ def poll_logs(self, project: ProjectModel, request: PollLogsRequest) -> JobSubmi next_token = str(current_line) break - except IOError as e: - raise LogStorageError(f"Failed to read log file {log_file_path}: {e}") - return JobSubmissionLogs(logs=logs, next_token=next_token) def write_logs( diff --git a/src/tests/_internal/server/services/test_logs.py b/src/tests/_internal/server/services/test_logs.py index 553b9dffe9..3ece736a7c 100644 --- a/src/tests/_internal/server/services/test_logs.py +++ b/src/tests/_internal/server/services/test_logs.py @@ -13,6 +13,7 @@ from pydantic import ValidationError from sqlalchemy.ext.asyncio import AsyncSession +from dstack._internal.core.errors import ServerClientError from dstack._internal.core.models.logs import LogEvent, LogEventSource from dstack._internal.server.models import ProjectModel from dstack._internal.server.schemas.logs import PollLogsRequest @@ -206,49 +207,22 @@ async def test_poll_logs_invalid_next_token_raises_error( limit=10, diagnose=True, ) - with pytest.raises( - LogStorageError, match="Invalid next_token: invalid. Must be a valid integer." - ): + with pytest.raises(ServerClientError): log_storage.poll_logs(project, poll_request) # Test with negative next_token poll_request.next_token = "-1" - with pytest.raises( - LogStorageError, match="Invalid next_token: -1. Must be a non-negative integer." - ): + with pytest.raises(ServerClientError): log_storage.poll_logs(project, poll_request) # Test with float next_token poll_request.next_token = "1.5" - with pytest.raises( - LogStorageError, match="Invalid next_token: 1.5. Must be a valid integer." - ): + with pytest.raises(ServerClientError): log_storage.poll_logs(project, poll_request) @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_poll_logs_descending_raises_error( - self, test_db, session: AsyncSession, tmp_path: Path - ): - project = await create_project(session=session) - log_storage = FileLogStorage(tmp_path) - - # Test that descending=True raises LogStorageError - poll_request = PollLogsRequest( - run_name="test_run", - job_submission_id=UUID("1b0e1b45-2f8c-4ab6-8010-a0d1a3e44e0e"), - limit=10, - diagnose=True, - # Note: This bypasses schema validation for testing the implementation - ) - poll_request.descending = True # Set directly to bypass validation - - with pytest.raises(LogStorageError, match="descending: true is not supported"): - log_storage.poll_logs(project, poll_request) - - @pytest.mark.asyncio - @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_poll_logs_file_not_found_raises_error( + async def test_poll_logs_file_not_found_raises_no_error( self, test_db, session: AsyncSession, tmp_path: Path ): project = await create_project(session=session) @@ -261,11 +235,7 @@ async def test_poll_logs_file_not_found_raises_error( limit=10, diagnose=True, ) - - with pytest.raises( - LogStorageError, match="Failed to read log file .* No such file or directory" - ): - log_storage.poll_logs(project, poll_request) + log_storage.poll_logs(project, poll_request) @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)