Skip to content

Commit 85d927e

Browse files
authored
Optimize db queries (dstackai#2928)
* Remove extra queries when processing running job * Optimize process_instances queries * Optimize process_runs queries * Optimize process_fleets queries * Optimize attribute loading * Select for update with of=InstanceModel * Remove extra selectin * Select for update with of=VolumeModel * Select with .load_only(InstanceModel.fleet_id) * Select with .load_only(FleetModel.name)
1 parent b58ad40 commit 85d927e

12 files changed

Lines changed: 239 additions & 173 deletions

src/dstack/_internal/server/background/tasks/process_fleets.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from dstack._internal.core.models.fleets import FleetStatus
99
from dstack._internal.server.db import get_db, get_session_ctx
10-
from dstack._internal.server.models import FleetModel
10+
from dstack._internal.server.models import FleetModel, InstanceModel, JobModel, RunModel
1111
from dstack._internal.server.services.fleets import (
1212
is_fleet_empty,
1313
is_fleet_in_use,
@@ -60,13 +60,14 @@ async def _process_next_fleet():
6060
async def _process_fleet(session: AsyncSession, fleet_model: FleetModel):
6161
logger.debug("Processing fleet %s", fleet_model.name)
6262
# Refetch to load related attributes.
63-
# joinedload produces LEFT OUTER JOIN that can't be used with FOR UPDATE.
6463
res = await session.execute(
6564
select(FleetModel)
6665
.where(FleetModel.id == fleet_model.id)
67-
.options(joinedload(FleetModel.project))
68-
.options(joinedload(FleetModel.instances))
69-
.options(joinedload(FleetModel.runs))
66+
.options(joinedload(FleetModel.instances).load_only(InstanceModel.deleted))
67+
.options(
68+
joinedload(FleetModel.instances).joinedload(InstanceModel.jobs).load_only(JobModel.id)
69+
)
70+
.options(joinedload(FleetModel.runs).load_only(RunModel.status))
7071
.execution_options(populate_existing=True)
7172
)
7273
fleet_model = res.unique().scalar_one()

src/dstack/_internal/server/background/tasks/process_gateways.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ async def _process_connection(conn: GatewayConnection):
110110
async def _process_submitted_gateway(session: AsyncSession, gateway_model: GatewayModel):
111111
logger.info("%s: started gateway provisioning", fmt(gateway_model))
112112
# Refetch to load related attributes.
113-
# joinedload produces LEFT OUTER JOIN that can't be used with FOR UPDATE.
114113
res = await session.execute(
115114
select(GatewayModel)
116115
.where(GatewayModel.id == gateway_model.id)
@@ -157,7 +156,6 @@ async def _process_provisioning_gateway(
157156
session: AsyncSession, gateway_model: GatewayModel
158157
) -> None:
159158
# Refetch to load related attributes.
160-
# joinedload produces LEFT OUTER JOIN that can't be used with FOR UPDATE.
161159
res = await session.execute(
162160
select(GatewayModel)
163161
.where(GatewayModel.id == gateway_model.id)

src/dstack/_internal/server/background/tasks/process_idle_volumes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from dstack._internal.core.models.profiles import parse_duration
1111
from dstack._internal.core.models.volumes import VolumeStatus
1212
from dstack._internal.server.db import get_db, get_session_ctx
13-
from dstack._internal.server.models import ProjectModel, VolumeModel
13+
from dstack._internal.server.models import ProjectModel, UserModel, VolumeModel
1414
from dstack._internal.server.services import backends as backends_services
1515
from dstack._internal.server.services.locking import get_locker
1616
from dstack._internal.server.services.volumes import (
@@ -49,7 +49,7 @@ async def process_idle_volumes():
4949
select(VolumeModel)
5050
.where(VolumeModel.id.in_(volume_ids))
5151
.options(joinedload(VolumeModel.project).joinedload(ProjectModel.backends))
52-
.options(joinedload(VolumeModel.user))
52+
.options(joinedload(VolumeModel.user).load_only(UserModel.name))
5353
.options(joinedload(VolumeModel.attachments))
5454
.execution_options(populate_existing=True)
5555
)

src/dstack/_internal/server/background/tasks/process_instances.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic import ValidationError
1010
from sqlalchemy import select
1111
from sqlalchemy.ext.asyncio import AsyncSession
12-
from sqlalchemy.orm import joinedload, lazyload
12+
from sqlalchemy.orm import joinedload
1313

1414
from dstack._internal import settings
1515
from dstack._internal.core.backends.base.compute import (
@@ -78,6 +78,7 @@
7878
from dstack._internal.server.models import (
7979
FleetModel,
8080
InstanceModel,
81+
JobModel,
8182
PlacementGroupModel,
8283
ProjectModel,
8384
)
@@ -106,6 +107,7 @@
106107
from dstack._internal.server.services.runner.ssh import runner_ssh_tunnel
107108
from dstack._internal.utils.common import (
108109
get_current_datetime,
110+
get_or_error,
109111
run_async,
110112
)
111113
from dstack._internal.utils.logging import get_logger
@@ -154,10 +156,11 @@ async def _process_next_instance():
154156
InstanceModel.last_processed_at
155157
< get_current_datetime() - MIN_PROCESSING_INTERVAL,
156158
)
157-
.options(lazyload(InstanceModel.jobs))
159+
.options(joinedload(InstanceModel.jobs).load_only(JobModel.id, JobModel.status))
160+
.options(joinedload(InstanceModel.project).load_only(ProjectModel.ssh_private_key))
158161
.order_by(InstanceModel.last_processed_at.asc())
159162
.limit(1)
160-
.with_for_update(skip_locked=True, key_share=True)
163+
.with_for_update(skip_locked=True, key_share=True, of=InstanceModel)
161164
)
162165
instance = res.scalar()
163166
if instance is None:
@@ -171,23 +174,22 @@ async def _process_next_instance():
171174

172175

173176
async def _process_instance(session: AsyncSession, instance: InstanceModel):
174-
# Refetch to load related attributes.
175-
# joinedload produces LEFT OUTER JOIN that can't be used with FOR UPDATE.
176-
res = await session.execute(
177-
select(InstanceModel)
178-
.where(InstanceModel.id == instance.id)
179-
.options(joinedload(InstanceModel.project).joinedload(ProjectModel.backends))
180-
.options(joinedload(InstanceModel.jobs))
181-
.options(joinedload(InstanceModel.fleet).joinedload(FleetModel.instances))
182-
.execution_options(populate_existing=True)
183-
)
184-
instance = res.unique().scalar_one()
185-
if (
186-
instance.status == InstanceStatus.IDLE
187-
and instance.termination_policy == TerminationPolicy.DESTROY_AFTER_IDLE
188-
and not instance.jobs
177+
if instance.status in (
178+
InstanceStatus.PENDING,
179+
InstanceStatus.TERMINATING,
189180
):
190-
await _mark_terminating_if_idle_duration_expired(instance)
181+
# Refetch to load related attributes.
182+
# Load related attributes only for statuses that always need them.
183+
res = await session.execute(
184+
select(InstanceModel)
185+
.where(InstanceModel.id == instance.id)
186+
.options(joinedload(InstanceModel.project).joinedload(ProjectModel.backends))
187+
.options(joinedload(InstanceModel.jobs).load_only(JobModel.id, JobModel.status))
188+
.options(joinedload(InstanceModel.fleet).joinedload(FleetModel.instances))
189+
.execution_options(populate_existing=True)
190+
)
191+
instance = res.unique().scalar_one()
192+
191193
if instance.status == InstanceStatus.PENDING:
192194
if instance.remote_connection_info is not None:
193195
await _add_remote(instance)
@@ -201,15 +203,23 @@ async def _process_instance(session: AsyncSession, instance: InstanceModel):
201203
InstanceStatus.IDLE,
202204
InstanceStatus.BUSY,
203205
):
204-
await _check_instance(instance)
206+
idle_duration_expired = _check_and_mark_terminating_if_idle_duration_expired(instance)
207+
if not idle_duration_expired:
208+
await _check_instance(session, instance)
205209
elif instance.status == InstanceStatus.TERMINATING:
206210
await _terminate(instance)
207211

208212
instance.last_processed_at = get_current_datetime()
209213
await session.commit()
210214

211215

212-
async def _mark_terminating_if_idle_duration_expired(instance: InstanceModel):
216+
def _check_and_mark_terminating_if_idle_duration_expired(instance: InstanceModel):
217+
if not (
218+
instance.status == InstanceStatus.IDLE
219+
and instance.termination_policy == TerminationPolicy.DESTROY_AFTER_IDLE
220+
and not instance.jobs
221+
):
222+
return False
213223
idle_duration = _get_instance_idle_duration(instance)
214224
idle_seconds = instance.termination_idle_time
215225
delta = datetime.timedelta(seconds=idle_seconds)
@@ -225,6 +235,8 @@ async def _mark_terminating_if_idle_duration_expired(instance: InstanceModel):
225235
"instance_status": instance.status.value,
226236
},
227237
)
238+
return True
239+
return False
228240

229241

230242
async def _add_remote(instance: InstanceModel) -> None:
@@ -703,7 +715,7 @@ def _mark_terminated(instance: InstanceModel, termination_reason: str) -> None:
703715
)
704716

705717

706-
async def _check_instance(instance: InstanceModel) -> None:
718+
async def _check_instance(session: AsyncSession, instance: InstanceModel) -> None:
707719
if (
708720
instance.status == InstanceStatus.BUSY
709721
and instance.jobs
@@ -722,12 +734,16 @@ async def _check_instance(instance: InstanceModel) -> None:
722734
)
723735
return
724736

725-
job_provisioning_data = JobProvisioningData.__response__.parse_raw(
726-
instance.job_provisioning_data
727-
)
737+
job_provisioning_data = get_or_error(get_instance_provisioning_data(instance))
728738
if job_provisioning_data.hostname is None:
739+
res = await session.execute(
740+
select(ProjectModel)
741+
.where(ProjectModel.id == instance.id)
742+
.options(joinedload(ProjectModel.backends))
743+
)
744+
project = res.scalar_one()
729745
await _wait_for_instance_provisioning_data(
730-
project=instance.project,
746+
project=project,
731747
instance=instance,
732748
job_provisioning_data=job_provisioning_data,
733749
)

src/dstack/_internal/server/background/tasks/process_metrics.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dstack._internal.core.models.runs import JobStatus
1010
from dstack._internal.server import settings
1111
from dstack._internal.server.db import get_session_ctx
12-
from dstack._internal.server.models import InstanceModel, JobMetricsPoint, JobModel
12+
from dstack._internal.server.models import InstanceModel, JobMetricsPoint, JobModel, ProjectModel
1313
from dstack._internal.server.schemas.runner import MetricsResponse
1414
from dstack._internal.server.services.instances import get_instance_ssh_private_keys
1515
from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
@@ -31,7 +31,11 @@ async def collect_metrics():
3131
res = await session.execute(
3232
select(JobModel)
3333
.where(JobModel.status.in_([JobStatus.RUNNING]))
34-
.options(joinedload(JobModel.instance).joinedload(InstanceModel.project))
34+
.options(
35+
joinedload(JobModel.instance)
36+
.joinedload(InstanceModel.project)
37+
.load_only(ProjectModel.ssh_private_key)
38+
)
3539
.order_by(JobModel.last_processed_at.asc())
3640
.limit(MAX_JOBS_FETCHED)
3741
)

src/dstack/_internal/server/background/tasks/process_prometheus_metrics.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from dstack._internal.core.consts import DSTACK_SHIM_HTTP_PORT
1010
from dstack._internal.core.models.runs import JobStatus
1111
from dstack._internal.server.db import get_session_ctx
12-
from dstack._internal.server.models import InstanceModel, JobModel, JobPrometheusMetrics
12+
from dstack._internal.server.models import (
13+
InstanceModel,
14+
JobModel,
15+
JobPrometheusMetrics,
16+
ProjectModel,
17+
)
1318
from dstack._internal.server.services.instances import get_instance_ssh_private_keys
1419
from dstack._internal.server.services.jobs import get_job_provisioning_data, get_job_runtime_data
1520
from dstack._internal.server.services.runner import client
@@ -43,7 +48,11 @@ async def collect_prometheus_metrics():
4348
JobPrometheusMetrics.collected_at < cutoff,
4449
),
4550
)
46-
.options(joinedload(JobModel.instance).joinedload(InstanceModel.project))
51+
.options(
52+
joinedload(JobModel.instance)
53+
.joinedload(InstanceModel.project)
54+
.load_only(ProjectModel.ssh_private_key)
55+
)
4756
.order_by(JobModel.last_processed_at.asc())
4857
.limit(MAX_JOBS_FETCHED)
4958
)

0 commit comments

Comments
 (0)