diff --git a/src/dstack/_internal/server/routers/runs.py b/src/dstack/_internal/server/routers/runs.py index a6523a1ba..ab468c16d 100644 --- a/src/dstack/_internal/server/routers/runs.py +++ b/src/dstack/_internal/server/routers/runs.py @@ -10,6 +10,7 @@ from dstack._internal.server.db import get_session from dstack._internal.server.models import ProjectModel, UserModel from dstack._internal.server.schemas.runs import ( + MAX_JOB_SUBMISSIONS_LIMIT, ApplyRunPlanRequest, DeleteRunsRequest, GetRunPlanRequest, @@ -61,10 +62,14 @@ async def list_runs( `project_name`, `repo_id`, `username`, and `only_active` can be specified as filters. Setting `only_active` to `true` excludes finished runs and deleted runs. Specifying `repo_id` without `project_name` returns no runs. + At most `job_submissions_limit` latest job submissions are returned per job. The results are paginated. To get the next page, pass `submitted_at` and `id` of the last run from the previous page as `prev_submitted_at` and `prev_run_id`. """ + job_submissions_limit = body.job_submissions_limit + if job_submissions_limit is None: + job_submissions_limit = MAX_JOB_SUBMISSIONS_LIMIT run_list = await runs.list_user_runs( session=session, user=user, @@ -73,7 +78,7 @@ async def list_runs( username=body.username, only_active=body.only_active, include_jobs=body.include_jobs, - job_submissions_limit=body.job_submissions_limit, + job_submissions_limit=job_submissions_limit, prev_submitted_at=body.prev_submitted_at, prev_run_id=body.prev_run_id, limit=body.limit, diff --git a/src/dstack/_internal/server/schemas/runs.py b/src/dstack/_internal/server/schemas/runs.py index 844724371..12b0c36ba 100644 --- a/src/dstack/_internal/server/schemas/runs.py +++ b/src/dstack/_internal/server/schemas/runs.py @@ -7,6 +7,8 @@ from dstack._internal.core.models.common import CoreModel from dstack._internal.core.models.runs import ApplyRunPlanInput, RunSpec +MAX_JOB_SUBMISSIONS_LIMIT = 10 + class ListRunsRequest(CoreModel): project_name: Optional[str] = None @@ -20,9 +22,11 @@ class ListRunsRequest(CoreModel): job_submissions_limit: Optional[int] = Field( None, ge=0, + le=MAX_JOB_SUBMISSIONS_LIMIT, description=( "Limit number of job submissions returned per job to avoid large responses." - "Drops older job submissions. No effect with `include_jobs: false`" + " Drops older job submissions. No effect with `include_jobs: false`." + f" Defaults to the maximum allowed value of {MAX_JOB_SUBMISSIONS_LIMIT}" ), ) prev_submitted_at: Optional[datetime] = None diff --git a/src/tests/_internal/server/routers/test_runs.py b/src/tests/_internal/server/routers/test_runs.py index e9aba1e8f..e98e0b150 100644 --- a/src/tests/_internal/server/routers/test_runs.py +++ b/src/tests/_internal/server/routers/test_runs.py @@ -48,7 +48,7 @@ from dstack._internal.core.models.users import GlobalRole, ProjectRole from dstack._internal.core.models.volumes import InstanceMountPoint, MountPoint from dstack._internal.server.models import JobModel, RunModel -from dstack._internal.server.schemas.runs import ApplyRunPlanRequest +from dstack._internal.server.schemas.runs import MAX_JOB_SUBMISSIONS_LIMIT, ApplyRunPlanRequest from dstack._internal.server.services.projects import add_project_member from dstack._internal.server.services.resources import ( set_gpu_vendor_default, @@ -983,6 +983,47 @@ async def test_limits_job_submissions( }, ] + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_limits_job_submissions_by_default( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session=session, global_role=GlobalRole.USER) + project = await create_project(session=session, owner=user) + await add_project_member( + session=session, project=project, user=user, project_role=ProjectRole.USER + ) + repo = await create_repo( + session=session, + project_id=project.id, + ) + run = await create_run( + session=session, + project=project, + repo=repo, + user=user, + ) + submissions_num = MAX_JOB_SUBMISSIONS_LIMIT + 2 + for submission_num in range(submissions_num): + await create_job( + session=session, + run=run, + submission_num=submission_num, + ) + response = await client.post( + "/api/runs/list", + headers=get_auth_headers(user.token), + json={}, + ) + assert response.status_code == 200, response.json() + runs = response.json() + assert len(runs) == 1 + job_submissions = runs[0]["jobs"][0]["job_submissions"] + assert len(job_submissions) == MAX_JOB_SUBMISSIONS_LIMIT + assert [js["submission_num"] for js in job_submissions] == list( + range(submissions_num - MAX_JOB_SUBMISSIONS_LIMIT, submissions_num) + ) + @pytest.mark.asyncio @pytest.mark.parametrize( "client_version,expected_probes",