dstack 0.19.19__py3-none-any.whl → 0.19.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dstack might be problematic. Click here for more details.

Files changed (54) hide show
  1. dstack/_internal/core/backends/__init__.py +0 -65
  2. dstack/_internal/core/backends/cloudrift/api_client.py +13 -1
  3. dstack/_internal/core/backends/features.py +64 -0
  4. dstack/_internal/core/backends/oci/resources.py +5 -5
  5. dstack/_internal/core/compatibility/fleets.py +2 -0
  6. dstack/_internal/core/compatibility/runs.py +4 -0
  7. dstack/_internal/core/models/profiles.py +37 -0
  8. dstack/_internal/server/app.py +22 -10
  9. dstack/_internal/server/background/__init__.py +5 -6
  10. dstack/_internal/server/background/tasks/process_fleets.py +52 -38
  11. dstack/_internal/server/background/tasks/process_gateways.py +2 -2
  12. dstack/_internal/server/background/tasks/process_idle_volumes.py +5 -4
  13. dstack/_internal/server/background/tasks/process_instances.py +62 -48
  14. dstack/_internal/server/background/tasks/process_metrics.py +9 -2
  15. dstack/_internal/server/background/tasks/process_placement_groups.py +2 -0
  16. dstack/_internal/server/background/tasks/process_prometheus_metrics.py +14 -2
  17. dstack/_internal/server/background/tasks/process_running_jobs.py +129 -124
  18. dstack/_internal/server/background/tasks/process_runs.py +63 -20
  19. dstack/_internal/server/background/tasks/process_submitted_jobs.py +12 -10
  20. dstack/_internal/server/background/tasks/process_terminating_jobs.py +12 -4
  21. dstack/_internal/server/background/tasks/process_volumes.py +4 -1
  22. dstack/_internal/server/migrations/versions/50dd7ea98639_index_status_columns.py +55 -0
  23. dstack/_internal/server/migrations/versions/ec02a26a256c_add_runmodel_next_triggered_at.py +38 -0
  24. dstack/_internal/server/models.py +16 -16
  25. dstack/_internal/server/schemas/logs.py +1 -9
  26. dstack/_internal/server/services/fleets.py +19 -10
  27. dstack/_internal/server/services/gateways/__init__.py +17 -17
  28. dstack/_internal/server/services/instances.py +10 -14
  29. dstack/_internal/server/services/jobs/__init__.py +10 -12
  30. dstack/_internal/server/services/logs/aws.py +45 -3
  31. dstack/_internal/server/services/logs/filelog.py +121 -11
  32. dstack/_internal/server/services/offers.py +3 -3
  33. dstack/_internal/server/services/projects.py +35 -15
  34. dstack/_internal/server/services/prometheus/client_metrics.py +3 -0
  35. dstack/_internal/server/services/prometheus/custom_metrics.py +22 -3
  36. dstack/_internal/server/services/runs.py +74 -34
  37. dstack/_internal/server/services/services/__init__.py +4 -1
  38. dstack/_internal/server/services/users.py +2 -3
  39. dstack/_internal/server/services/volumes.py +11 -11
  40. dstack/_internal/server/settings.py +3 -0
  41. dstack/_internal/server/statics/index.html +1 -1
  42. dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js → main-39a767528976f8078166.js} +7 -26
  43. dstack/_internal/server/statics/{main-64f8273740c4b52c18f5.js.map → main-39a767528976f8078166.js.map} +1 -1
  44. dstack/_internal/server/statics/{main-d58fc0460cb0eae7cb5c.css → main-8f9ee218d3eb45989682.css} +2 -2
  45. dstack/_internal/server/testing/common.py +7 -0
  46. dstack/_internal/server/utils/sentry_utils.py +12 -0
  47. dstack/_internal/utils/common.py +10 -21
  48. dstack/_internal/utils/cron.py +5 -0
  49. dstack/version.py +1 -1
  50. {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/METADATA +2 -11
  51. {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/RECORD +54 -49
  52. {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/WHEEL +0 -0
  53. {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/entry_points.txt +0 -0
  54. {dstack-0.19.19.dist-info → dstack-0.19.21.dist-info}/licenses/LICENSE.md +0 -0
@@ -1,7 +1,7 @@
1
1
  import itertools
2
+ import json
2
3
  from collections import defaultdict
3
4
  from collections.abc import Generator, Iterable
4
- from datetime import timezone
5
5
  from typing import ClassVar
6
6
  from uuid import UUID
7
7
 
@@ -79,7 +79,7 @@ async def get_instance_metrics(session: AsyncSession) -> Iterable[Metric]:
79
79
  "dstack_backend": instance.backend.value if instance.backend is not None else "",
80
80
  "dstack_gpu": gpu,
81
81
  }
82
- duration = (now - instance.created_at.replace(tzinfo=timezone.utc)).total_seconds()
82
+ duration = (now - instance.created_at).total_seconds()
83
83
  metrics.add_sample(_INSTANCE_DURATION, labels, duration)
84
84
  metrics.add_sample(_INSTANCE_PRICE, labels, instance.price or 0.0)
85
85
  metrics.add_sample(_INSTANCE_GPU_COUNT, labels, gpu_count)
@@ -166,7 +166,7 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
166
166
  "dstack_backend": jpd.get_base_backend().value,
167
167
  "dstack_gpu": gpus[0].name if gpus else "",
168
168
  }
169
- duration = (now - job.submitted_at.replace(tzinfo=timezone.utc)).total_seconds()
169
+ duration = (now - job.submitted_at).total_seconds()
170
170
  metrics.add_sample(_JOB_DURATION, labels, duration)
171
171
  metrics.add_sample(_JOB_PRICE, labels, price)
172
172
  metrics.add_sample(_JOB_GPU_COUNT, labels, len(gpus))
@@ -177,6 +177,19 @@ async def get_job_metrics(session: AsyncSession) -> Iterable[Metric]:
177
177
  metrics.add_sample(_JOB_CPU_TIME, labels, jmp.cpu_usage_micro / 1_000_000)
178
178
  metrics.add_sample(_JOB_MEMORY_USAGE, labels, jmp.memory_usage_bytes)
179
179
  metrics.add_sample(_JOB_MEMORY_WORKING_SET, labels, jmp.memory_working_set_bytes)
180
+ if gpus:
181
+ gpu_memory_total = gpus[0].memory_mib * 1024 * 1024
182
+ for gpu_num, (gpu_util, gpu_memory_usage) in enumerate(
183
+ zip(
184
+ json.loads(jmp.gpus_util_percent),
185
+ json.loads(jmp.gpus_memory_usage_bytes),
186
+ )
187
+ ):
188
+ gpu_labels = labels.copy()
189
+ gpu_labels["dstack_gpu_num"] = gpu_num
190
+ metrics.add_sample(_JOB_GPU_USAGE_RATIO, gpu_labels, gpu_util / 100)
191
+ metrics.add_sample(_JOB_GPU_MEMORY_TOTAL, gpu_labels, gpu_memory_total)
192
+ metrics.add_sample(_JOB_GPU_MEMORY_USAGE, gpu_labels, gpu_memory_usage)
180
193
  jpm = job_prometheus_metrics.get(job.id)
181
194
  if jpm is not None:
182
195
  for metric in text_string_to_metric_families(jpm.text):
@@ -202,6 +215,9 @@ _JOB_CPU_TIME = "dstack_job_cpu_time_seconds_total"
202
215
  _JOB_MEMORY_TOTAL = "dstack_job_memory_total_bytes"
203
216
  _JOB_MEMORY_USAGE = "dstack_job_memory_usage_bytes"
204
217
  _JOB_MEMORY_WORKING_SET = "dstack_job_memory_working_set_bytes"
218
+ _JOB_GPU_USAGE_RATIO = "dstack_job_gpu_usage_ratio"
219
+ _JOB_GPU_MEMORY_TOTAL = "dstack_job_gpu_memory_total_bytes"
220
+ _JOB_GPU_MEMORY_USAGE = "dstack_job_gpu_memory_usage_bytes"
205
221
 
206
222
 
207
223
  class _Metrics(dict[str, Metric]):
@@ -259,6 +275,9 @@ class _JobMetrics(_Metrics):
259
275
  (_JOB_MEMORY_TOTAL, _GAUGE, "Total memory allocated for the job, bytes"),
260
276
  (_JOB_MEMORY_USAGE, _GAUGE, "Memory used by the job (including cache), bytes"),
261
277
  (_JOB_MEMORY_WORKING_SET, _GAUGE, "Memory used by the job (not including cache), bytes"),
278
+ (_JOB_GPU_USAGE_RATIO, _GAUGE, "Job GPU usage, percent (as 0.0-1.0)"),
279
+ (_JOB_GPU_MEMORY_TOTAL, _GAUGE, "Total GPU memory allocated for the job, bytes"),
280
+ (_JOB_GPU_MEMORY_USAGE, _GAUGE, "GPU memory used by the job, bytes"),
262
281
  ]
263
282
 
264
283
 
@@ -5,9 +5,10 @@ from datetime import datetime, timezone
5
5
  from typing import List, Optional
6
6
 
7
7
  import pydantic
8
+ from apscheduler.triggers.cron import CronTrigger
8
9
  from sqlalchemy import and_, func, or_, select, update
9
10
  from sqlalchemy.ext.asyncio import AsyncSession
10
- from sqlalchemy.orm import joinedload, selectinload
11
+ from sqlalchemy.orm import joinedload
11
12
 
12
13
  import dstack._internal.utils.common as common_utils
13
14
  from dstack._internal.core.errors import (
@@ -42,7 +43,6 @@ from dstack._internal.core.models.runs import (
42
43
  RunTerminationReason,
43
44
  ServiceSpec,
44
45
  )
45
- from dstack._internal.core.models.users import GlobalRole
46
46
  from dstack._internal.core.models.volumes import (
47
47
  InstanceMountPoint,
48
48
  Volume,
@@ -81,7 +81,7 @@ from dstack._internal.server.services.locking import get_locker, string_to_lock_
81
81
  from dstack._internal.server.services.logging import fmt
82
82
  from dstack._internal.server.services.offers import get_offers_by_requirements
83
83
  from dstack._internal.server.services.plugins import apply_plugin_policies
84
- from dstack._internal.server.services.projects import list_project_models, list_user_project_models
84
+ from dstack._internal.server.services.projects import list_user_project_models
85
85
  from dstack._internal.server.services.resources import set_resources_defaults
86
86
  from dstack._internal.server.services.secrets import get_project_secrets_mapping
87
87
  from dstack._internal.server.services.users import get_user_model_by_name
@@ -115,10 +115,11 @@ async def list_user_runs(
115
115
  ) -> List[Run]:
116
116
  if project_name is None and repo_id is not None:
117
117
  return []
118
- if user.global_role == GlobalRole.ADMIN:
119
- projects = await list_project_models(session=session)
120
- else:
121
- projects = await list_user_project_models(session=session, user=user)
118
+ projects = await list_user_project_models(
119
+ session=session,
120
+ user=user,
121
+ only_names=True,
122
+ )
122
123
  runs_user = None
123
124
  if username is not None:
124
125
  runs_user = await get_user_model_by_name(session=session, username=username)
@@ -217,9 +218,9 @@ async def list_projects_run_models(
217
218
  res = await session.execute(
218
219
  select(RunModel)
219
220
  .where(*filters)
221
+ .options(joinedload(RunModel.user).load_only(UserModel.name))
220
222
  .order_by(*order_by)
221
223
  .limit(limit)
222
- .options(selectinload(RunModel.user))
223
224
  )
224
225
  run_models = list(res.scalars().all())
225
226
  return run_models
@@ -511,6 +512,14 @@ async def submit_run(
511
512
  )
512
513
 
513
514
  submitted_at = common_utils.get_current_datetime()
515
+ initial_status = RunStatus.SUBMITTED
516
+ initial_replicas = 1
517
+ if run_spec.merged_profile.schedule is not None:
518
+ initial_status = RunStatus.PENDING
519
+ initial_replicas = 0
520
+ elif run_spec.configuration.type == "service":
521
+ initial_replicas = run_spec.configuration.replicas.min
522
+
514
523
  run_model = RunModel(
515
524
  id=uuid.uuid4(),
516
525
  project_id=project.id,
@@ -519,21 +528,20 @@ async def submit_run(
519
528
  user_id=user.id,
520
529
  run_name=run_spec.run_name,
521
530
  submitted_at=submitted_at,
522
- status=RunStatus.SUBMITTED,
531
+ status=initial_status,
523
532
  run_spec=run_spec.json(),
524
533
  last_processed_at=submitted_at,
525
534
  priority=run_spec.configuration.priority,
526
535
  deployment_num=0,
527
536
  desired_replica_count=1, # a relevant value will be set in process_runs.py
537
+ next_triggered_at=_get_next_triggered_at(run_spec),
528
538
  )
529
539
  session.add(run_model)
530
540
 
531
- replicas = 1
532
541
  if run_spec.configuration.type == "service":
533
- replicas = run_spec.configuration.replicas.min
534
542
  await services.register_service(session, run_model, run_spec)
535
543
 
536
- for replica_num in range(replicas):
544
+ for replica_num in range(initial_replicas):
537
545
  jobs = await get_jobs_from_run_spec(
538
546
  run_spec=run_spec,
539
547
  secrets=secrets,
@@ -693,8 +701,8 @@ def run_model_to_run(
693
701
  id=run_model.id,
694
702
  project_name=run_model.project.name,
695
703
  user=run_model.user.name,
696
- submitted_at=run_model.submitted_at.replace(tzinfo=timezone.utc),
697
- last_processed_at=run_model.last_processed_at.replace(tzinfo=timezone.utc),
704
+ submitted_at=run_model.submitted_at,
705
+ last_processed_at=run_model.last_processed_at,
698
706
  status=run_model.status,
699
707
  status_message=status_message,
700
708
  termination_reason=run_model.termination_reason,
@@ -972,6 +980,12 @@ def _validate_run_spec_and_set_defaults(run_spec: RunSpec):
972
980
  raise ServerClientError(
973
981
  f"Maximum utilization_policy.time_window is {settings.SERVER_METRICS_RUNNING_TTL_SECONDS}s"
974
982
  )
983
+ if (
984
+ run_spec.merged_profile.schedule
985
+ and run_spec.configuration.type == "service"
986
+ and run_spec.configuration.replicas.min == 0
987
+ ):
988
+ raise ServerClientError("Scheduled services with autoscaling to zero are not supported")
975
989
  if run_spec.configuration.priority is None:
976
990
  run_spec.configuration.priority = RUN_PRIORITY_DEFAULT
977
991
  set_resources_defaults(run_spec.configuration.resources)
@@ -1059,7 +1073,7 @@ def _check_can_update_configuration(
1059
1073
  )
1060
1074
 
1061
1075
 
1062
- async def process_terminating_run(session: AsyncSession, run: RunModel):
1076
+ async def process_terminating_run(session: AsyncSession, run_model: RunModel):
1063
1077
  """
1064
1078
  Used by both `process_runs` and `stop_run` to process a TERMINATING run.
1065
1079
  Stops the jobs gracefully and marks them as TERMINATING.
@@ -1067,44 +1081,54 @@ async def process_terminating_run(session: AsyncSession, run: RunModel):
1067
1081
  When all jobs are terminated, assigns a finished status to the run.
1068
1082
  Caller must acquire the lock on run.
1069
1083
  """
1070
- assert run.termination_reason is not None
1071
- job_termination_reason = run.termination_reason.to_job_termination_reason()
1084
+ assert run_model.termination_reason is not None
1085
+ run = run_model_to_run(run_model, include_jobs=False)
1086
+ job_termination_reason = run_model.termination_reason.to_job_termination_reason()
1072
1087
 
1073
1088
  unfinished_jobs_count = 0
1074
- for job in run.jobs:
1075
- if job.status.is_finished():
1089
+ for job_model in run_model.jobs:
1090
+ if job_model.status.is_finished():
1076
1091
  continue
1077
1092
  unfinished_jobs_count += 1
1078
- if job.status == JobStatus.TERMINATING:
1093
+ if job_model.status == JobStatus.TERMINATING:
1079
1094
  if job_termination_reason == JobTerminationReason.ABORTED_BY_USER:
1080
1095
  # Override termination reason so that
1081
1096
  # abort actions such as volume force detach are triggered
1082
- job.termination_reason = job_termination_reason
1097
+ job_model.termination_reason = job_termination_reason
1083
1098
  continue
1084
1099
 
1085
- if job.status == JobStatus.RUNNING and job_termination_reason not in {
1100
+ if job_model.status == JobStatus.RUNNING and job_termination_reason not in {
1086
1101
  JobTerminationReason.ABORTED_BY_USER,
1087
1102
  JobTerminationReason.DONE_BY_RUNNER,
1088
1103
  }:
1089
1104
  # Send a signal to stop the job gracefully
1090
- await stop_runner(session, job)
1091
- delay_job_instance_termination(job)
1092
- job.status = JobStatus.TERMINATING
1093
- job.termination_reason = job_termination_reason
1094
- job.last_processed_at = common_utils.get_current_datetime()
1105
+ await stop_runner(session, job_model)
1106
+ delay_job_instance_termination(job_model)
1107
+ job_model.status = JobStatus.TERMINATING
1108
+ job_model.termination_reason = job_termination_reason
1109
+ job_model.last_processed_at = common_utils.get_current_datetime()
1095
1110
 
1096
1111
  if unfinished_jobs_count == 0:
1097
- if run.service_spec is not None:
1112
+ if run_model.service_spec is not None:
1098
1113
  try:
1099
- await services.unregister_service(session, run)
1114
+ await services.unregister_service(session, run_model)
1100
1115
  except Exception as e:
1101
- logger.warning("%s: failed to unregister service: %s", fmt(run), repr(e))
1102
- run.status = run.termination_reason.to_status()
1116
+ logger.warning("%s: failed to unregister service: %s", fmt(run_model), repr(e))
1117
+ if (
1118
+ run.run_spec.merged_profile.schedule is not None
1119
+ and run_model.termination_reason
1120
+ not in [RunTerminationReason.ABORTED_BY_USER, RunTerminationReason.STOPPED_BY_USER]
1121
+ ):
1122
+ run_model.next_triggered_at = _get_next_triggered_at(run.run_spec)
1123
+ run_model.status = RunStatus.PENDING
1124
+ else:
1125
+ run_model.status = run_model.termination_reason.to_status()
1126
+
1103
1127
  logger.info(
1104
1128
  "%s: run status has changed TERMINATING -> %s, reason: %s",
1105
- fmt(run),
1106
- run.status.name,
1107
- run.termination_reason.name,
1129
+ fmt(run_model),
1130
+ run_model.status.name,
1131
+ run_model.termination_reason.name,
1108
1132
  )
1109
1133
 
1110
1134
 
@@ -1224,3 +1248,19 @@ async def retry_run_replica_jobs(
1224
1248
 
1225
1249
  def _remove_job_spec_sensitive_info(spec: JobSpec):
1226
1250
  spec.ssh_key = None
1251
+
1252
+
1253
+ def _get_next_triggered_at(run_spec: RunSpec) -> Optional[datetime]:
1254
+ if run_spec.merged_profile.schedule is None:
1255
+ return None
1256
+ now = common_utils.get_current_datetime()
1257
+ fire_times = []
1258
+ for cron in run_spec.merged_profile.schedule.crons:
1259
+ cron_trigger = CronTrigger.from_crontab(cron, timezone=timezone.utc)
1260
+ fire_times.append(
1261
+ cron_trigger.get_next_fire_time(
1262
+ previous_fire_time=None,
1263
+ now=now,
1264
+ )
1265
+ )
1266
+ return min(fire_times)
@@ -28,6 +28,7 @@ from dstack._internal.server.models import GatewayModel, JobModel, ProjectModel,
28
28
  from dstack._internal.server.services.gateways import (
29
29
  get_gateway_configuration,
30
30
  get_or_add_gateway_connection,
31
+ get_project_default_gateway_model,
31
32
  get_project_gateway_model_by_name,
32
33
  )
33
34
  from dstack._internal.server.services.logging import fmt
@@ -52,7 +53,9 @@ async def register_service(session: AsyncSession, run_model: RunModel, run_spec:
52
53
  elif run_spec.configuration.gateway == False:
53
54
  gateway = None
54
55
  else:
55
- gateway = run_model.project.default_gateway
56
+ gateway = await get_project_default_gateway_model(
57
+ session=session, project=run_model.project
58
+ )
56
59
 
57
60
  if gateway is not None:
58
61
  service_spec = await _register_service_in_gateway(session, run_model, run_spec, gateway)
@@ -2,7 +2,6 @@ import hashlib
2
2
  import os
3
3
  import re
4
4
  import uuid
5
- from datetime import timezone
6
5
  from typing import Awaitable, Callable, List, Optional, Tuple
7
6
 
8
7
  from sqlalchemy import delete, select, update
@@ -195,7 +194,7 @@ def user_model_to_user(user_model: UserModel) -> User:
195
194
  return User(
196
195
  id=user_model.id,
197
196
  username=user_model.name,
198
- created_at=user_model.created_at.replace(tzinfo=timezone.utc),
197
+ created_at=user_model.created_at,
199
198
  global_role=user_model.global_role,
200
199
  email=user_model.email,
201
200
  active=user_model.active,
@@ -207,7 +206,7 @@ def user_model_to_user_with_creds(user_model: UserModel) -> UserWithCreds:
207
206
  return UserWithCreds(
208
207
  id=user_model.id,
209
208
  username=user_model.name,
210
- created_at=user_model.created_at.replace(tzinfo=timezone.utc),
209
+ created_at=user_model.created_at,
211
210
  global_role=user_model.global_role,
212
211
  email=user_model.email,
213
212
  active=user_model.active,
@@ -1,19 +1,18 @@
1
1
  import uuid
2
- from datetime import datetime, timedelta, timezone
2
+ from datetime import datetime, timedelta
3
3
  from typing import List, Optional
4
4
 
5
5
  from sqlalchemy import and_, func, or_, select, update
6
6
  from sqlalchemy.ext.asyncio import AsyncSession
7
7
  from sqlalchemy.orm import joinedload, selectinload
8
8
 
9
- from dstack._internal.core.backends import BACKENDS_WITH_VOLUMES_SUPPORT
10
9
  from dstack._internal.core.backends.base.compute import ComputeWithVolumeSupport
10
+ from dstack._internal.core.backends.features import BACKENDS_WITH_VOLUMES_SUPPORT
11
11
  from dstack._internal.core.errors import (
12
12
  BackendNotAvailable,
13
13
  ResourceExistsError,
14
14
  ServerClientError,
15
15
  )
16
- from dstack._internal.core.models.users import GlobalRole
17
16
  from dstack._internal.core.models.volumes import (
18
17
  Volume,
19
18
  VolumeAttachment,
@@ -40,7 +39,7 @@ from dstack._internal.server.services.locking import (
40
39
  string_to_lock_id,
41
40
  )
42
41
  from dstack._internal.server.services.plugins import apply_plugin_policies
43
- from dstack._internal.server.services.projects import list_project_models, list_user_project_models
42
+ from dstack._internal.server.services.projects import list_user_project_models
44
43
  from dstack._internal.utils import common, random_names
45
44
  from dstack._internal.utils.logging import get_logger
46
45
 
@@ -57,10 +56,11 @@ async def list_volumes(
57
56
  limit: int,
58
57
  ascending: bool,
59
58
  ) -> List[Volume]:
60
- if user.global_role == GlobalRole.ADMIN:
61
- projects = await list_project_models(session=session)
62
- else:
63
- projects = await list_user_project_models(session=session, user=user)
59
+ projects = await list_user_project_models(
60
+ session=session,
61
+ user=user,
62
+ only_names=True,
63
+ )
64
64
  if project_name is not None:
65
65
  projects = [p for p in projects if p.name == project_name]
66
66
  volume_models = await list_projects_volume_models(
@@ -320,15 +320,15 @@ def volume_model_to_volume(volume_model: VolumeModel) -> Volume:
320
320
  )
321
321
  deleted_at = None
322
322
  if volume_model.deleted_at is not None:
323
- deleted_at = volume_model.deleted_at.replace(tzinfo=timezone.utc)
323
+ deleted_at = volume_model.deleted_at
324
324
  volume = Volume(
325
325
  name=volume_model.name,
326
326
  project_name=volume_model.project.name,
327
327
  user=volume_model.user.name,
328
328
  configuration=configuration,
329
329
  external=configuration.volume_id is not None,
330
- created_at=volume_model.created_at.replace(tzinfo=timezone.utc),
331
- last_processed_at=volume_model.last_processed_at.replace(tzinfo=timezone.utc),
330
+ created_at=volume_model.created_at,
331
+ last_processed_at=volume_model.last_processed_at,
332
332
  status=volume_model.status,
333
333
  status_message=volume_model.status_message,
334
334
  deleted=volume_model.deleted,
@@ -93,6 +93,9 @@ DEFAULT_PROJECT_NAME = "main"
93
93
 
94
94
  SENTRY_DSN = os.getenv("DSTACK_SENTRY_DSN")
95
95
  SENTRY_TRACES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_TRACES_SAMPLE_RATE", 0.1))
96
+ SENTRY_TRACES_BACKGROUND_SAMPLE_RATE = float(
97
+ os.getenv("DSTACK_SENTRY_TRACES_BACKGROUND_SAMPLE_RATE", 0.01)
98
+ )
96
99
  SENTRY_PROFILES_SAMPLE_RATE = float(os.getenv("DSTACK_SENTRY_PROFILES_SAMPLE_RATE", 0))
97
100
 
98
101
  DEFAULT_CREDS_DISABLED = os.getenv("DSTACK_DEFAULT_CREDS_DISABLED") is not None
@@ -1,3 +1,3 @@
1
1
  <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>dstack</title><meta name="description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
2
2
  "/><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"><meta name="og:title" content="dstack"><meta name="og:type" content="article"><meta name="og:image" content="/splash_thumbnail.png"><meta name="og:description" content="Get GPUs at the best prices and availability from a wide range of providers. No cloud account of your own is required.
3
- "><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-64f8273740c4b52c18f5.js"></script><link href="/main-d58fc0460cb0eae7cb5c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>
3
+ "><link rel="icon" type="image/x-icon" href="/assets/favicon.ico"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon-48x48.png"><link rel="manifest" href="/assets/manifest.webmanifest"><meta name="mobile-web-app-capable" content="yes"><meta name="theme-color" content="#fff"><meta name="application-name" content="dstackai"><link rel="apple-touch-icon" sizes="57x57" href="/assets/apple-touch-icon-57x57.png"><link rel="apple-touch-icon" sizes="60x60" href="/assets/apple-touch-icon-60x60.png"><link rel="apple-touch-icon" sizes="72x72" href="/assets/apple-touch-icon-72x72.png"><link rel="apple-touch-icon" sizes="76x76" href="/assets/apple-touch-icon-76x76.png"><link rel="apple-touch-icon" sizes="114x114" href="/assets/apple-touch-icon-114x114.png"><link rel="apple-touch-icon" sizes="120x120" href="/assets/apple-touch-icon-120x120.png"><link rel="apple-touch-icon" sizes="144x144" href="/assets/apple-touch-icon-144x144.png"><link rel="apple-touch-icon" sizes="152x152" href="/assets/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="167x167" href="/assets/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="1024x1024" href="/assets/apple-touch-icon-1024x1024.png"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="apple-mobile-web-app-title" content="dstackai"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-640x1136.png"><link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1136x640.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-750x1334.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1334x750.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1125x2436.png"><link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2436x1125.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1170x2532.png"><link rel="apple-touch-startup-image" media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2532x1170.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1179x2556.png"><link rel="apple-touch-startup-image" media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2556x1179.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-828x1792.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-1792x828.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2688.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2688x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1242x2208.png"><link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2208x1242.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1284x2778.png"><link rel="apple-touch-startup-image" media="(device-width: 428px) and (device-height: 926px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2778x1284.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1290x2796.png"><link rel="apple-touch-startup-image" media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2796x1290.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1488x2266.png"><link rel="apple-touch-startup-image" media="(device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2266x1488.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1536x2048.png"><link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2048x1536.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1620x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1620.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1640x2160.png"><link rel="apple-touch-startup-image" media="(device-width: 820px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2160x1640.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2388.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2388x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-1668x2224.png"><link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2224x1668.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="/assets/apple-touch-startup-image-2048x2732.png"><link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)" href="/assets/apple-touch-startup-image-2732x2048.png"><meta name="msapplication-TileColor" content="#fff"><meta name="msapplication-TileImage" content="/assets/mstile-144x144.png"><meta name="msapplication-config" content="/assets/browserconfig.xml"><link rel="yandex-tableau-widget" href="/assets/yandex-browser-manifest.json"><script defer="defer" src="/main-39a767528976f8078166.js"></script><link href="/main-8f9ee218d3eb45989682.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div class="b-page-header" id="header"></div><div id="root"></div></body></html>