diracx-db 0.0.1a33__py3-none-any.whl → 0.0.1a35__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,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from DIRAC.Core.Utilities import TimeUtilities
4
+
3
5
  from diracx.db.os.utils import BaseOSDB
4
6
 
5
7
 
@@ -18,8 +20,17 @@ class JobParametersDB(BaseOSDB):
18
20
  "Status": {"type": "keyword"},
19
21
  "JobType": {"type": "keyword"},
20
22
  }
21
- index_prefix = "mysetup_elasticjobparameters_index_"
23
+ # TODO: Does this need to be configurable?
24
+ index_prefix = "job_parameters"
25
+
26
+ def index_name(self, vo, doc_id: int) -> str:
27
+ split = int(int(doc_id) // 1e6)
28
+ return f"{self.index_prefix}_{vo}_{split}m"
22
29
 
23
- def index_name(self, doc_id: int) -> str:
24
- # TODO: Remove setup and replace "123.0m" with "120m"?
25
- return f"{self.index_prefix}_{doc_id // 1e6:.1f}m"
30
+ def upsert(self, vo, doc_id, document):
31
+ document = {
32
+ "JobID": doc_id,
33
+ "timestamp": TimeUtilities.toEpochMilliSeconds(),
34
+ **document,
35
+ }
36
+ return super().upsert(vo, doc_id, document)
diracx/db/os/utils.py CHANGED
@@ -77,7 +77,7 @@ class BaseOSDB(metaclass=ABCMeta):
77
77
  index_prefix: str
78
78
 
79
79
  @abstractmethod
80
- def index_name(self, doc_id: int) -> str: ...
80
+ def index_name(self, vo: str, doc_id: int) -> str: ...
81
81
 
82
82
  def __init__(self, connection_kwargs: dict[str, Any]) -> None:
83
83
  self._client: AsyncOpenSearch | None = None
@@ -182,15 +182,20 @@ class BaseOSDB(metaclass=ABCMeta):
182
182
  )
183
183
  assert result["acknowledged"]
184
184
 
185
- async def upsert(self, doc_id, document) -> None:
186
- # TODO: Implement properly
185
+ async def upsert(self, vo: str, doc_id: int, document: Any) -> None:
186
+ index_name = self.index_name(vo, doc_id)
187
187
  response = await self.client.update(
188
- index=self.index_name(doc_id),
188
+ index=index_name,
189
189
  id=doc_id,
190
190
  body={"doc": document, "doc_as_upsert": True},
191
191
  params=dict(retry_on_conflict=10),
192
192
  )
193
- print(f"{response=}")
193
+ logger.debug(
194
+ "Upserted document %s in index %s with response: %s",
195
+ doc_id,
196
+ index_name,
197
+ response,
198
+ )
194
199
 
195
200
  async def search(
196
201
  self, parameters, search, sorts, *, per_page: int = 100, page: int | None = None
diracx/db/sql/job/db.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ __all__ = ["JobDB"]
4
+
3
5
  from datetime import datetime, timezone
4
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING, Any, Iterable
5
7
 
6
8
  from sqlalchemy import bindparam, case, delete, func, insert, select, update
7
9
 
@@ -9,13 +11,12 @@ if TYPE_CHECKING:
9
11
  from sqlalchemy.sql.elements import BindParameter
10
12
 
11
13
  from diracx.core.exceptions import InvalidQueryError
12
- from diracx.core.models import (
13
- SearchSpec,
14
- SortSpec,
15
- )
14
+ from diracx.core.models import JobCommand, SearchSpec, SortSpec
16
15
 
17
16
  from ..utils import BaseSQLDB, apply_search_filters, apply_sort_constraints
17
+ from ..utils.functions import utcnow
18
18
  from .schema import (
19
+ HeartBeatLoggingInfo,
19
20
  InputData,
20
21
  JobCommands,
21
22
  JobDBBase,
@@ -38,6 +39,16 @@ def _get_columns(table, parameters):
38
39
  class JobDB(BaseSQLDB):
39
40
  metadata = JobDBBase.metadata
40
41
 
42
+ # Field names which should be stored in the HeartBeatLoggingInfo table
43
+ heartbeat_fields = {
44
+ "LoadAverage",
45
+ "MemoryUsed",
46
+ "Vsize",
47
+ "AvailableDiskSpace",
48
+ "CPUConsumed",
49
+ "WallClockTime",
50
+ }
51
+
41
52
  # TODO: this is copied from the DIRAC JobDB
42
53
  # but is overwriten in LHCbDIRAC, so we need
43
54
  # to find a way to make it dynamic
@@ -156,6 +167,24 @@ class JobDB(BaseSQLDB):
156
167
  ],
157
168
  )
158
169
 
170
+ @staticmethod
171
+ def _set_job_attributes_fix_value(column, value):
172
+ """Apply corrections to the values before inserting them into the database.
173
+
174
+ TODO: Move this logic into the sqlalchemy model.
175
+ """
176
+ if column == "VerifiedFlag":
177
+ value_str = str(value)
178
+ if value_str in ("True", "False"):
179
+ return value_str
180
+ if column == "AccountedFlag":
181
+ value_str = str(value)
182
+ if value_str in ("True", "False", "Failed"):
183
+ return value_str
184
+ else:
185
+ return value
186
+ raise NotImplementedError(f"Unrecognized value for column {column}: {value}")
187
+
159
188
  async def set_job_attributes(self, job_data):
160
189
  """Update the parameters of the given jobs."""
161
190
  # TODO: add myDate and force parameters.
@@ -168,7 +197,10 @@ class JobDB(BaseSQLDB):
168
197
  case_expressions = {
169
198
  column: case(
170
199
  *[
171
- (Jobs.__table__.c.JobID == job_id, attrs[column])
200
+ (
201
+ Jobs.__table__.c.JobID == job_id,
202
+ self._set_job_attributes_fix_value(column, attrs[column]),
203
+ )
172
204
  for job_id, attrs in job_data.items()
173
205
  if column in attrs
174
206
  ],
@@ -197,7 +229,7 @@ class JobDB(BaseSQLDB):
197
229
 
198
230
  return {jobid: jdl for jobid, jdl in (await self.conn.execute(stmt)) if jdl}
199
231
 
200
- async def set_job_commands(self, commands: list[tuple[int, str, str]]):
232
+ async def set_job_commands(self, commands: list[tuple[int, str, str]]) -> None:
201
233
  """Store a command to be passed to the job together with the next heart beat."""
202
234
  await self.conn.execute(
203
235
  insert(JobCommands),
@@ -246,3 +278,58 @@ class JobDB(BaseSQLDB):
246
278
  rows = await self.conn.execute(stmt, update_parameters)
247
279
 
248
280
  return rows.rowcount
281
+
282
+ async def add_heartbeat_data(
283
+ self, job_id: int, dynamic_data: dict[str, str]
284
+ ) -> None:
285
+ """Add the job's heartbeat data to the database.
286
+
287
+ NOTE: This does not update the HeartBeatTime column in the Jobs table.
288
+ This is instead handled by the `diracx.logic.jobs.status.set_job_statuses`
289
+ as it involves updating multiple databases.
290
+
291
+ :param job_id: the job id
292
+ :param dynamic_data: mapping of the dynamic data to store,
293
+ e.g. {"AvailableDiskSpace": 123}
294
+ """
295
+ if extra_fields := set(dynamic_data) - self.heartbeat_fields:
296
+ raise InvalidQueryError(
297
+ f"Not allowed to store heartbeat data for: {extra_fields}. "
298
+ f"Allowed keys are: {self.heartbeat_fields}"
299
+ )
300
+ values = [
301
+ {
302
+ "JobID": job_id,
303
+ "Name": key,
304
+ "Value": value,
305
+ "HeartBeatTime": utcnow(),
306
+ }
307
+ for key, value in dynamic_data.items()
308
+ ]
309
+ await self.conn.execute(HeartBeatLoggingInfo.__table__.insert().values(values))
310
+
311
+ async def get_job_commands(self, job_ids: Iterable[int]) -> list[JobCommand]:
312
+ """Get a command to be passed to the job together with the next heartbeat.
313
+
314
+ :param job_ids: the job ids
315
+ :return: mapping of job id to list of commands
316
+ """
317
+ # Get the commands
318
+ stmt = (
319
+ select(JobCommands.job_id, JobCommands.command, JobCommands.arguments)
320
+ .where(JobCommands.job_id.in_(job_ids), JobCommands.status == "Received")
321
+ .order_by(JobCommands.job_id)
322
+ )
323
+ commands = await self.conn.execute(stmt)
324
+ # Update the status of the commands
325
+ stmt = (
326
+ update(JobCommands)
327
+ .where(JobCommands.job_id.in_(job_ids))
328
+ .values(Status="Sent")
329
+ )
330
+ await self.conn.execute(stmt)
331
+ # Return the commands grouped by job id
332
+ return [
333
+ JobCommand(job_id=cmd.JobID, command=cmd.Command, arguments=cmd.Arguments)
334
+ for cmd in commands
335
+ ]
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sqlalchemy.types as types
3
4
  from sqlalchemy import (
4
5
  DateTime,
5
- Enum,
6
6
  ForeignKey,
7
7
  Index,
8
8
  Integer,
@@ -16,6 +16,36 @@ from ..utils import Column, EnumBackedBool, NullColumn
16
16
  JobDBBase = declarative_base()
17
17
 
18
18
 
19
+ class AccountedFlagEnum(types.TypeDecorator):
20
+ """Maps a ``AccountedFlagEnum()`` column to True/False in Python."""
21
+
22
+ impl = types.Enum
23
+ cache_ok: bool = True
24
+
25
+ def __init__(self) -> None:
26
+ super().__init__("True", "False", "Failed")
27
+
28
+ def process_bind_param(self, value, dialect) -> str:
29
+ if value is True:
30
+ return "True"
31
+ elif value is False:
32
+ return "False"
33
+ elif value == "Failed":
34
+ return "Failed"
35
+ else:
36
+ raise NotImplementedError(value, dialect)
37
+
38
+ def process_result_value(self, value, dialect) -> bool | str:
39
+ if value == "True":
40
+ return True
41
+ elif value == "False":
42
+ return False
43
+ elif value == "Failed":
44
+ return "Failed"
45
+ else:
46
+ raise NotImplementedError(f"Unknown {value=}")
47
+
48
+
19
49
  class Jobs(JobDBBase):
20
50
  __tablename__ = "Jobs"
21
51
 
@@ -45,10 +75,7 @@ class Jobs(JobDBBase):
45
75
  user_priority = Column("UserPriority", Integer, default=0)
46
76
  reschedule_counter = Column("RescheduleCounter", Integer, default=0)
47
77
  verified_flag = Column("VerifiedFlag", EnumBackedBool(), default=False)
48
- # TODO: Should this be True/False/"Failed"? Or True/False/Null?
49
- accounted_flag = Column(
50
- "AccountedFlag", Enum("True", "False", "Failed"), default="False"
51
- )
78
+ accounted_flag = Column("AccountedFlag", AccountedFlagEnum(), default=False)
52
79
 
53
80
  __table_args__ = (
54
81
  Index("JobType", "JobType"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diracx-db
3
- Version: 0.0.1a33
3
+ Version: 0.0.1a35
4
4
  Summary: TODO
5
5
  License: GPL-3.0-only
6
6
  Classifier: Intended Audience :: Science/Research
@@ -3,8 +3,8 @@ 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=Knca19uT2G-5FI7MOFlaOAXeHn4ecPVLIH30TiwhaTw,858
7
- diracx/db/os/utils.py,sha256=xQLhOgC9YidfeEIbmdigXg3_YZLKHwMcZzhHf4mRwyQ,11399
6
+ diracx/db/os/job_parameters.py,sha256=W-2O0cnq91iExcQAEqnCKuA9Ko_6MQaTGALGG8ziQ-4,1149
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
10
10
  diracx/db/sql/auth/db.py,sha256=O9sp-AKobQ4X4IF2WaJKig-8LU8Ra9Syn5jW8JNLRU4,9030
@@ -13,8 +13,8 @@ diracx/db/sql/dummy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
13
13
  diracx/db/sql/dummy/db.py,sha256=whcF02IjFQTwe1l4iTHtCkg2XukEaLHpLeHYNGMuGGw,1645
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=qoK1toWekRP1BO5XbT9CaRuGKR9_oixMVfRZ9myZsjg,8644
17
- diracx/db/sql/job/schema.py,sha256=Rmt5ldVN6Pf4MJHMhiT_Zn-k9LVPBTVivmGlyPKFRZE,4647
16
+ diracx/db/sql/job/db.py,sha256=PTivVrSl4hrOTgy77WECunPJUJiYzGhSlqO0wYwEuE8,11909
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=0WuRn3pKEeuV9IZ_3VDhlvqI9eM1W4C0dZwIVZKW9N8,5811
20
20
  diracx/db/sql/job_logging/schema.py,sha256=DyFr_4goxfC453xPJX4OkxTGHtlP_5q8PXpr8bhNQS0,960
@@ -31,8 +31,8 @@ diracx/db/sql/utils/__init__.py,sha256=QkvpqBuIAgkAOywAssYzdxSzUQVZlSUumK7mPxotX
31
31
  diracx/db/sql/utils/base.py,sha256=7UxHBNLOSjdrIdslMKW4C_c5H9-6Y1BEimxscri2poE,12367
32
32
  diracx/db/sql/utils/functions.py,sha256=iLqlUIQ6SrDUtDEnZ5szaFbdcINJW15KNbCdGXss6kc,3055
33
33
  diracx/db/sql/utils/types.py,sha256=yU-tXsu6hFGPsr9ba1n3ZjGPnHQI_06lbpkTeDCWJtg,1287
34
- diracx_db-0.0.1a33.dist-info/METADATA,sha256=gMmJC7ijyqAcDSjopiSPacDI4FXIFAs7xvELywUHGA0,644
35
- diracx_db-0.0.1a33.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
36
- diracx_db-0.0.1a33.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
- diracx_db-0.0.1a33.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
38
- diracx_db-0.0.1a33.dist-info/RECORD,,
34
+ diracx_db-0.0.1a35.dist-info/METADATA,sha256=9KlPHZrT1qCFKxJMs6uagTjk8VHZBbKRls7sw2dHqog,644
35
+ diracx_db-0.0.1a35.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
36
+ diracx_db-0.0.1a35.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
+ diracx_db-0.0.1a35.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
38
+ diracx_db-0.0.1a35.dist-info/RECORD,,