diracx-db 0.0.1a44__py3-none-any.whl → 0.0.1a45__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.
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from DIRAC.Core.Utilities import TimeUtilities
3
+ from datetime import UTC, datetime
4
4
 
5
5
  from diracx.db.os.utils import BaseOSDB
6
6
 
@@ -31,7 +31,7 @@ class JobParametersDB(BaseOSDB):
31
31
  def upsert(self, vo, doc_id, document):
32
32
  document = {
33
33
  "JobID": doc_id,
34
- "timestamp": TimeUtilities.toEpochMilliSeconds(),
34
+ "timestamp": int(datetime.now(tz=UTC).timestamp() * 1000),
35
35
  **document,
36
36
  }
37
37
  return super().upsert(vo, doc_id, document)
diracx/db/sql/job/db.py CHANGED
@@ -50,7 +50,7 @@ class JobDB(BaseSQLDB):
50
50
  }
51
51
 
52
52
  # TODO: this is copied from the DIRAC JobDB
53
- # but is overwriten in LHCbDIRAC, so we need
53
+ # but is overwritten in LHCbDIRAC, so we need
54
54
  # to find a way to make it dynamic
55
55
  jdl_2_db_parameters = ["JobName", "JobType", "JobGroup"]
56
56
 
@@ -44,7 +44,7 @@ class MagicEpochDateTime(TypeDecorator):
44
44
  """
45
45
  if value is None:
46
46
  return None
47
- # Carefully convert from Decimal to datetime to avoid loosing precision
47
+ # Carefully convert from Decimal to datetime to avoid losing precision
48
48
  value += self.MAGIC_EPOC_NUMBER
49
49
  value_int = int(value)
50
50
  result = datetime.fromtimestamp(value_int, tz=UTC)
@@ -1,8 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from functools import partial
6
+ from typing import Any, AsyncGenerator
4
7
 
5
- from sqlalchemy import Executable, delete, insert, literal, select, update
8
+ from sqlalchemy import (
9
+ BigInteger,
10
+ Column,
11
+ Executable,
12
+ MetaData,
13
+ Table,
14
+ and_,
15
+ delete,
16
+ insert,
17
+ literal,
18
+ or_,
19
+ select,
20
+ update,
21
+ )
6
22
  from sqlalchemy.exc import IntegrityError, NoResultFound
7
23
 
8
24
  from diracx.core.exceptions import (
@@ -12,15 +28,25 @@ from diracx.core.exceptions import (
12
28
  )
13
29
  from diracx.core.models import SandboxInfo, SandboxType, UserInfo
14
30
  from diracx.db.sql.utils.base import BaseSQLDB
15
- from diracx.db.sql.utils.functions import utcnow
31
+ from diracx.db.sql.utils.functions import days_since, utcnow
16
32
 
17
33
  from .schema import Base as SandboxMetadataDBBase
18
34
  from .schema import SandBoxes, SBEntityMapping, SBOwners
19
35
 
36
+ logger = logging.getLogger(__name__)
37
+
20
38
 
21
39
  class SandboxMetadataDB(BaseSQLDB):
22
40
  metadata = SandboxMetadataDBBase.metadata
23
41
 
42
+ # Temporary table to store the sandboxes to delete, see `select_and_delete_expired`
43
+ _temp_table = Table(
44
+ "sb_to_delete",
45
+ MetaData(),
46
+ Column("SBId", BigInteger, primary_key=True),
47
+ prefixes=["TEMPORARY"],
48
+ )
49
+
24
50
  async def get_owner_id(self, user: UserInfo) -> int | None:
25
51
  """Get the id of the owner from the database."""
26
52
  stmt = select(SBOwners.OwnerID).where(
@@ -85,7 +111,13 @@ class SandboxMetadataDB(BaseSQLDB):
85
111
  .values(LastAccessTime=utcnow())
86
112
  )
87
113
  result = await self.conn.execute(stmt)
88
- assert result.rowcount == 1
114
+ if result.rowcount == 0:
115
+ # If the update didn't affect any row, the sandbox doesn't exist
116
+ raise SandboxNotFoundError(pfn, se_name)
117
+ elif result.rowcount != 1:
118
+ raise NotImplementedError(
119
+ "More than one sandbox was updated. This should not happen."
120
+ )
89
121
 
90
122
  async def sandbox_is_assigned(self, pfn: str, se_name: str) -> bool | None:
91
123
  """Checks if a sandbox exists and has been assigned."""
@@ -128,7 +160,7 @@ class SandboxMetadataDB(BaseSQLDB):
128
160
  sb_type: SandboxType,
129
161
  se_name: str,
130
162
  ) -> None:
131
- """Mapp sandbox and jobs."""
163
+ """Map sandbox and jobs."""
132
164
  for job_id in jobs_ids:
133
165
  # Define the entity id as 'Entity:entity_id' due to the DB definition:
134
166
  entity_id = self.jobid_to_entity_id(job_id)
@@ -187,3 +219,56 @@ class SandboxMetadataDB(BaseSQLDB):
187
219
  .values(Assigned=False)
188
220
  )
189
221
  await self.conn.execute(unassign_stmt)
222
+
223
+ @asynccontextmanager
224
+ async def delete_unused_sandboxes(
225
+ self, *, limit: int | None = None
226
+ ) -> AsyncGenerator[AsyncGenerator[str, None], None]:
227
+ """Get the sandbox PFNs to delete.
228
+
229
+ The result of this function can be used as an async context manager
230
+ to yield the PFNs of the sandboxes to delete. The context manager
231
+ will automatically remove the sandboxes from the database upon exit.
232
+
233
+ Args:
234
+ limit: If not None, the maximum number of sandboxes to delete.
235
+
236
+ """
237
+ conditions = [
238
+ # If it has assigned to a job but is no longer mapped it can be removed
239
+ # and_(SandBoxes.Assigned, ~exists(SandBoxes.SBId == SBEntityMapping.SBId)),
240
+ # If the sandbox is still unassigned after 15 days, remove it
241
+ and_(~SandBoxes.Assigned, days_since(SandBoxes.LastAccessTime) >= 15),
242
+ ]
243
+ # Sandboxes which are not on S3 will be handled by legacy DIRAC
244
+ condition = and_(SandBoxes.SEPFN.like("/S3/%"), or_(*conditions))
245
+
246
+ # Copy the in-flight rows to a temporary table
247
+ await self.conn.run_sync(partial(self._temp_table.create, checkfirst=True))
248
+ select_stmt = select(SandBoxes.SBId).where(condition)
249
+ if limit:
250
+ select_stmt = select_stmt.limit(limit)
251
+ insert_stmt = insert(self._temp_table).from_select(["SBId"], select_stmt)
252
+ await self.conn.execute(insert_stmt)
253
+
254
+ try:
255
+ # Select the sandbox PFNs from the temporary table and yield them
256
+ select_stmt = select(SandBoxes.SEPFN).join(
257
+ self._temp_table, self._temp_table.c.SBId == SandBoxes.SBId
258
+ )
259
+
260
+ async def yield_pfns() -> AsyncGenerator[str, None]:
261
+ async for row in await self.conn.stream(select_stmt):
262
+ yield row.SEPFN
263
+
264
+ yield yield_pfns()
265
+
266
+ # Delete the sandboxes from the main table
267
+ delete_stmt = delete(SandBoxes).where(
268
+ SandBoxes.SBId.in_(select(self._temp_table.c.SBId))
269
+ )
270
+ result = await self.conn.execute(delete_stmt)
271
+ logger.info("Deleted %d expired/unassigned sandboxes", result.rowcount)
272
+
273
+ finally:
274
+ await self.conn.run_sync(partial(self._temp_table.drop, checkfirst=True))
@@ -56,7 +56,7 @@ class TaskQueueDB(BaseSQLDB):
56
56
  )
57
57
  rows = await self.conn.execute(stmt)
58
58
  # Get owners in this group and the amount of times they appear
59
- # TODO: I guess the rows are already a list of tupes
59
+ # TODO: I guess the rows are already a list of tuples
60
60
  # maybe refactor
61
61
  return {r[0]: r[1] for r in rows if r}
62
62
 
@@ -108,7 +108,7 @@ class TaskQueueDB(BaseSQLDB):
108
108
  tq_ids: list[int],
109
109
  priority: float,
110
110
  ):
111
- """Set the priority for a user/userGroup combo given a splitted share."""
111
+ """Set the priority for a user/userGroup combo given a split share."""
112
112
  update_stmt = (
113
113
  update(TaskQueues)
114
114
  .where(TaskQueues.TQId.in_(tq_ids))
@@ -51,7 +51,7 @@ class BaseSQLDB(metaclass=ABCMeta):
51
51
  of the same database. To list the available implementations use
52
52
  `BaseSQLDB.available_implementations(db_name)`. The first entry in this list
53
53
  will be the preferred implementation and it can be initialized by calling
54
- it's `__init__` function with a URL perviously obtained from
54
+ it's `__init__` function with a URL previously obtained from
55
55
  `BaseSQLDB.available_urls`.
56
56
 
57
57
  To control the lifetime of the SQLAlchemy engine used for connecting to the
@@ -103,6 +103,37 @@ def sqlite_date_trunc(element, compiler, **kw):
103
103
  )
104
104
 
105
105
 
106
+ class days_since(expression.FunctionElement): # noqa: N801
107
+ """Sqlalchemy function to get the number of days since a given date.
108
+
109
+ Primarily used to be able to query for a specific resolution of a date e.g.
110
+
111
+ select * from table where days_since(date_column) = 0
112
+ select * from table where days_since(date_column) = 1
113
+ """
114
+
115
+ type = DateTime()
116
+ inherit_cache = False
117
+
118
+ def __init__(self, *args, **kwargs) -> None:
119
+ super().__init__(*args, **kwargs)
120
+
121
+
122
+ @compiles(days_since, "postgresql")
123
+ def pg_days_since(element, compiler, **kw):
124
+ return f"EXTRACT(DAY FROM (now() - {compiler.process(element.clauses)}))"
125
+
126
+
127
+ @compiles(days_since, "mysql")
128
+ def mysql_days_since(element, compiler, **kw):
129
+ return f"DATEDIFF(NOW(), {compiler.process(element.clauses)})"
130
+
131
+
132
+ @compiles(days_since, "sqlite")
133
+ def sqlite_days_since(element, compiler, **kw):
134
+ return f"julianday('now') - julianday({compiler.process(element.clauses)})"
135
+
136
+
106
137
  def substract_date(**kwargs: float) -> datetime:
107
138
  return datetime.now(tz=timezone.utc) - timedelta(**kwargs)
108
139
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diracx-db
3
- Version: 0.0.1a44
3
+ Version: 0.0.1a45
4
4
  Summary: TODO
5
5
  License: GPL-3.0-only
6
6
  Classifier: Intended Audience :: Science/Research
@@ -9,11 +9,11 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Topic :: Scientific/Engineering
10
10
  Classifier: Topic :: System :: Distributed Computing
11
11
  Requires-Python: >=3.11
12
- Description-Content-Type: text/markdown
13
12
  Requires-Dist: diracx-core
14
13
  Requires-Dist: opensearch-py[async]
15
14
  Requires-Dist: pydantic>=2.10
16
15
  Requires-Dist: sqlalchemy[aiomysql,aiosqlite]>=2
17
16
  Requires-Dist: uuid-utils
18
17
  Provides-Extra: testing
19
- Requires-Dist: diracx-testing; extra == "testing"
18
+ Requires-Dist: diracx-testing; extra == 'testing'
19
+ Requires-Dist: freezegun; extra == 'testing'
@@ -3,7 +3,7 @@ diracx/db/__main__.py,sha256=tU4tp3OAClYCiPMxlRj524sZGBx9oy4CoWHd8pMuEEs,1715
3
3
  diracx/db/exceptions.py,sha256=1nn-SZLG-nQwkxbvHjZqXhE5ouzWj1f3qhSda2B4ZEg,83
4
4
  diracx/db/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  diracx/db/os/__init__.py,sha256=IZr6z6SefrRvuC8sTC4RmB3_wwOyEt1GzpDuwSMH8O4,112
6
- diracx/db/os/job_parameters.py,sha256=YKW5Mx0_TB6cPJoLi08SJWckjXsedRDLq9QbVSQb5CM,1228
6
+ diracx/db/os/job_parameters.py,sha256=loAc-bo3u-RMAp_H1g8VRt8T-rCCsXp_d9aCvg5OS-A,1225
7
7
  diracx/db/os/utils.py,sha256=V4T-taos64SFNcorfIr7mq5l5y88K6TzyCj1YqWk8VI,11562
8
8
  diracx/db/sql/__init__.py,sha256=JYu0b0IVhoXy3lX2m2r2dmAjsRS7IbECBUMEDvX0Te4,391
9
9
  diracx/db/sql/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -13,26 +13,25 @@ diracx/db/sql/dummy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
13
13
  diracx/db/sql/dummy/db.py,sha256=IW4FzG7ERKbhZvC32KL7Rodu2u-zKAf8BryO4VAdJew,1650
14
14
  diracx/db/sql/dummy/schema.py,sha256=9zI53pKlzc6qBezsyjkatOQrNZdGCjwgjQ8Iz_pyAXs,789
15
15
  diracx/db/sql/job/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- diracx/db/sql/job/db.py,sha256=PTivVrSl4hrOTgy77WECunPJUJiYzGhSlqO0wYwEuE8,11909
16
+ diracx/db/sql/job/db.py,sha256=TnEc0fckiuMJAZg2v1_Pbwfn7kDPDam6TXp9ySuiddk,11910
17
17
  diracx/db/sql/job/schema.py,sha256=eFgZshe6NEzOM2qI0HI9Y3abrqDMoQIwa9L0vZugHcU,5431
18
18
  diracx/db/sql/job_logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  diracx/db/sql/job_logging/db.py,sha256=BYzlPuvdvHR7wdzQEVWMH_V5kL0bLBZtQkcugnSGbjs,5497
20
- diracx/db/sql/job_logging/schema.py,sha256=W2VeE7czBXF6uP2hmjBDur90S4BlidzFXX2V_WZkfzU,2525
20
+ diracx/db/sql/job_logging/schema.py,sha256=k6uBw-RHAcJ5GEleNpiWoXEJBhCiNG-y4xAgBKHZjjM,2524
21
21
  diracx/db/sql/pilot_agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  diracx/db/sql/pilot_agents/db.py,sha256=7-cuCbh_KhM0jlybsHMWV-W66bHsPHIVBpbuqwjncj0,1232
23
23
  diracx/db/sql/pilot_agents/schema.py,sha256=KeWnFSpYOTrT3-_rOCFjbjNnPNXKnUZiJVsu4vv5U2U,2149
24
24
  diracx/db/sql/sandbox_metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- diracx/db/sql/sandbox_metadata/db.py,sha256=1Nd0gNbPu2F21DYGQkFCLhTeKgUA26GE_vxDcdzUzwQ,7052
25
+ diracx/db/sql/sandbox_metadata/db.py,sha256=DAmuk-PDGKq3eLV2EovDGnJI0GDiH5u8C74ARIy8MWo,10171
26
26
  diracx/db/sql/sandbox_metadata/schema.py,sha256=V5gV2PHwzTbBz_th9ribLfE7Lqk8YGemDmvqq4jWQJ4,1530
27
27
  diracx/db/sql/task_queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- diracx/db/sql/task_queue/db.py,sha256=Ymd9cBasiSAPmgu5846B8KmguRiJ_GoJhuOev9YwW2o,6248
28
+ diracx/db/sql/task_queue/db.py,sha256=2qul1D2tX2uCI92N591WK5xWHakG0pNibzDwKQ7W-I8,6246
29
29
  diracx/db/sql/task_queue/schema.py,sha256=5efAgvNYRkLlaJ2NzRInRfmVa3tyIzQu2l0oRPy4Kzw,3258
30
30
  diracx/db/sql/utils/__init__.py,sha256=QkvpqBuIAgkAOywAssYzdxSzUQVZlSUumK7mPxotXfM,547
31
- diracx/db/sql/utils/base.py,sha256=7UxHBNLOSjdrIdslMKW4C_c5H9-6Y1BEimxscri2poE,12367
32
- diracx/db/sql/utils/functions.py,sha256=iLqlUIQ6SrDUtDEnZ5szaFbdcINJW15KNbCdGXss6kc,3055
31
+ diracx/db/sql/utils/base.py,sha256=HYQuX16mgg9LAMtAEmbTmJFIN0OSMe1Hcb57dtl7LCc,12367
32
+ diracx/db/sql/utils/functions.py,sha256=_E4tc9Gti6LuSh7QEyoqPJSvCuByVqvRenOXCzxsulE,4014
33
33
  diracx/db/sql/utils/types.py,sha256=yU-tXsu6hFGPsr9ba1n3ZjGPnHQI_06lbpkTeDCWJtg,1287
34
- diracx_db-0.0.1a44.dist-info/METADATA,sha256=GTQNVgaV7dVycm3qPuHFl32-EkLmeUr4ahtCgPyNbfQ,670
35
- diracx_db-0.0.1a44.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
36
- diracx_db-0.0.1a44.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
- diracx_db-0.0.1a44.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
38
- diracx_db-0.0.1a44.dist-info/RECORD,,
34
+ diracx_db-0.0.1a45.dist-info/METADATA,sha256=ET2Uo-DegfUc6qRqvicSGR7PpWTtc4yXJKqoZQRbXyQ,675
35
+ diracx_db-0.0.1a45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ diracx_db-0.0.1a45.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
+ diracx_db-0.0.1a45.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1 +0,0 @@
1
- diracx