diracx-db 0.0.1a16__py3-none-any.whl → 0.0.1a18__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.
- diracx/db/os/utils.py +60 -11
- diracx/db/sql/__init__.py +3 -1
- diracx/db/sql/auth/db.py +10 -19
- diracx/db/sql/auth/schema.py +5 -7
- diracx/db/sql/dummy/db.py +2 -3
- diracx/db/sql/{jobs → job}/db.py +12 -452
- diracx/db/sql/{jobs → job}/schema.py +2 -118
- diracx/db/sql/job_logging/__init__.py +0 -0
- diracx/db/sql/job_logging/db.py +161 -0
- diracx/db/sql/job_logging/schema.py +25 -0
- diracx/db/sql/sandbox_metadata/db.py +12 -10
- diracx/db/sql/task_queue/__init__.py +0 -0
- diracx/db/sql/task_queue/db.py +261 -0
- diracx/db/sql/task_queue/schema.py +109 -0
- diracx/db/sql/utils/__init__.py +418 -0
- diracx/db/sql/{jobs/status_utility.py → utils/job_status.py} +12 -19
- {diracx_db-0.0.1a16.dist-info → diracx_db-0.0.1a18.dist-info}/METADATA +5 -5
- diracx_db-0.0.1a18.dist-info/RECORD +33 -0
- {diracx_db-0.0.1a16.dist-info → diracx_db-0.0.1a18.dist-info}/WHEEL +1 -1
- diracx/db/sql/utils.py +0 -234
- diracx_db-0.0.1a16.dist-info/RECORD +0 -27
- /diracx/db/sql/{jobs → job}/__init__.py +0 -0
- {diracx_db-0.0.1a16.dist-info → diracx_db-0.0.1a18.dist-info}/entry_points.txt +0 -0
- {diracx_db-0.0.1a16.dist-info → diracx_db-0.0.1a18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from sqlalchemy import delete, func, insert, select
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
pass
|
11
|
+
|
12
|
+
from diracx.core.exceptions import JobNotFound
|
13
|
+
from diracx.core.models import (
|
14
|
+
JobStatus,
|
15
|
+
JobStatusReturn,
|
16
|
+
)
|
17
|
+
|
18
|
+
from ..utils import BaseSQLDB
|
19
|
+
from .schema import (
|
20
|
+
JobLoggingDBBase,
|
21
|
+
LoggingInfo,
|
22
|
+
)
|
23
|
+
|
24
|
+
MAGIC_EPOC_NUMBER = 1270000000
|
25
|
+
|
26
|
+
|
27
|
+
class JobLoggingDB(BaseSQLDB):
|
28
|
+
"""Frontend for the JobLoggingDB. Provides the ability to store changes with timestamps."""
|
29
|
+
|
30
|
+
metadata = JobLoggingDBBase.metadata
|
31
|
+
|
32
|
+
async def insert_record(
|
33
|
+
self,
|
34
|
+
job_id: int,
|
35
|
+
status: JobStatus,
|
36
|
+
minor_status: str,
|
37
|
+
application_status: str,
|
38
|
+
date: datetime,
|
39
|
+
source: str,
|
40
|
+
):
|
41
|
+
"""Add a new entry to the JobLoggingDB table. One, two or all the three status
|
42
|
+
components (status, minorStatus, applicationStatus) can be specified.
|
43
|
+
Optionally the time stamp of the status can
|
44
|
+
be provided in a form of a string in a format '%Y-%m-%d %H:%M:%S' or
|
45
|
+
as datetime.datetime object. If the time stamp is not provided the current
|
46
|
+
UTC time is used.
|
47
|
+
"""
|
48
|
+
# First, fetch the maximum SeqNum for the given job_id
|
49
|
+
seqnum_stmt = select(func.coalesce(func.max(LoggingInfo.SeqNum) + 1, 1)).where(
|
50
|
+
LoggingInfo.JobID == job_id
|
51
|
+
)
|
52
|
+
seqnum = await self.conn.scalar(seqnum_stmt)
|
53
|
+
|
54
|
+
epoc = (
|
55
|
+
time.mktime(date.timetuple())
|
56
|
+
+ date.microsecond / 1000000.0
|
57
|
+
- MAGIC_EPOC_NUMBER
|
58
|
+
)
|
59
|
+
|
60
|
+
stmt = insert(LoggingInfo).values(
|
61
|
+
JobID=int(job_id),
|
62
|
+
SeqNum=seqnum,
|
63
|
+
Status=status,
|
64
|
+
MinorStatus=minor_status,
|
65
|
+
ApplicationStatus=application_status[:255],
|
66
|
+
StatusTime=date,
|
67
|
+
StatusTimeOrder=epoc,
|
68
|
+
Source=source[:32],
|
69
|
+
)
|
70
|
+
await self.conn.execute(stmt)
|
71
|
+
|
72
|
+
async def get_records(self, job_id: int) -> list[JobStatusReturn]:
|
73
|
+
"""Returns a Status,MinorStatus,ApplicationStatus,StatusTime,Source tuple
|
74
|
+
for each record found for job specified by its jobID in historical order.
|
75
|
+
"""
|
76
|
+
stmt = (
|
77
|
+
select(
|
78
|
+
LoggingInfo.Status,
|
79
|
+
LoggingInfo.MinorStatus,
|
80
|
+
LoggingInfo.ApplicationStatus,
|
81
|
+
LoggingInfo.StatusTime,
|
82
|
+
LoggingInfo.Source,
|
83
|
+
)
|
84
|
+
.where(LoggingInfo.JobID == int(job_id))
|
85
|
+
.order_by(LoggingInfo.StatusTimeOrder, LoggingInfo.StatusTime)
|
86
|
+
)
|
87
|
+
rows = await self.conn.execute(stmt)
|
88
|
+
|
89
|
+
values = []
|
90
|
+
for (
|
91
|
+
status,
|
92
|
+
minor_status,
|
93
|
+
application_status,
|
94
|
+
status_time,
|
95
|
+
status_source,
|
96
|
+
) in rows:
|
97
|
+
values.append(
|
98
|
+
[
|
99
|
+
status,
|
100
|
+
minor_status,
|
101
|
+
application_status,
|
102
|
+
status_time.replace(tzinfo=timezone.utc),
|
103
|
+
status_source,
|
104
|
+
]
|
105
|
+
)
|
106
|
+
|
107
|
+
# If no value has been set for the application status in the first place,
|
108
|
+
# We put this status to unknown
|
109
|
+
res = []
|
110
|
+
if values:
|
111
|
+
if values[0][2] == "idem":
|
112
|
+
values[0][2] = "Unknown"
|
113
|
+
|
114
|
+
# We replace "idem" values by the value previously stated
|
115
|
+
for i in range(1, len(values)):
|
116
|
+
for j in range(3):
|
117
|
+
if values[i][j] == "idem":
|
118
|
+
values[i][j] = values[i - 1][j]
|
119
|
+
|
120
|
+
# And we replace arrays with tuples
|
121
|
+
for (
|
122
|
+
status,
|
123
|
+
minor_status,
|
124
|
+
application_status,
|
125
|
+
status_time,
|
126
|
+
status_source,
|
127
|
+
) in values:
|
128
|
+
res.append(
|
129
|
+
JobStatusReturn(
|
130
|
+
Status=status,
|
131
|
+
MinorStatus=minor_status,
|
132
|
+
ApplicationStatus=application_status,
|
133
|
+
StatusTime=status_time,
|
134
|
+
Source=status_source,
|
135
|
+
)
|
136
|
+
)
|
137
|
+
|
138
|
+
return res
|
139
|
+
|
140
|
+
async def delete_records(self, job_ids: list[int]):
|
141
|
+
"""Delete logging records for given jobs."""
|
142
|
+
stmt = delete(LoggingInfo).where(LoggingInfo.JobID.in_(job_ids))
|
143
|
+
await self.conn.execute(stmt)
|
144
|
+
|
145
|
+
async def get_wms_time_stamps(self, job_id):
|
146
|
+
"""Get TimeStamps for job MajorState transitions
|
147
|
+
return a {State:timestamp} dictionary.
|
148
|
+
"""
|
149
|
+
result = {}
|
150
|
+
stmt = select(
|
151
|
+
LoggingInfo.Status,
|
152
|
+
LoggingInfo.StatusTimeOrder,
|
153
|
+
).where(LoggingInfo.JobID == job_id)
|
154
|
+
rows = await self.conn.execute(stmt)
|
155
|
+
if not rows.rowcount:
|
156
|
+
raise JobNotFound(job_id) from None
|
157
|
+
|
158
|
+
for event, etime in rows:
|
159
|
+
result[event] = str(etime + MAGIC_EPOC_NUMBER)
|
160
|
+
|
161
|
+
return result
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from sqlalchemy import (
|
2
|
+
Integer,
|
3
|
+
Numeric,
|
4
|
+
PrimaryKeyConstraint,
|
5
|
+
String,
|
6
|
+
)
|
7
|
+
from sqlalchemy.orm import declarative_base
|
8
|
+
|
9
|
+
from ..utils import Column, DateNowColumn
|
10
|
+
|
11
|
+
JobLoggingDBBase = declarative_base()
|
12
|
+
|
13
|
+
|
14
|
+
class LoggingInfo(JobLoggingDBBase):
|
15
|
+
__tablename__ = "LoggingInfo"
|
16
|
+
JobID = Column(Integer)
|
17
|
+
SeqNum = Column(Integer)
|
18
|
+
Status = Column(String(32), default="")
|
19
|
+
MinorStatus = Column(String(128), default="")
|
20
|
+
ApplicationStatus = Column(String(255), default="")
|
21
|
+
StatusTime = DateNowColumn()
|
22
|
+
# TODO: Check that this corresponds to the DOUBLE(12,3) type in MySQL
|
23
|
+
StatusTimeOrder = Column(Numeric(precision=12, scale=3), default=0)
|
24
|
+
Source = Column(String(32), default="Unknown", name="StatusSource")
|
25
|
+
__table_args__ = (PrimaryKeyConstraint("JobID", "SeqNum"),)
|
@@ -15,7 +15,7 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
15
15
|
metadata = SandboxMetadataDBBase.metadata
|
16
16
|
|
17
17
|
async def upsert_owner(self, user: UserInfo) -> int:
|
18
|
-
"""Get the id of the owner from the database"""
|
18
|
+
"""Get the id of the owner from the database."""
|
19
19
|
# TODO: Follow https://github.com/DIRACGrid/diracx/issues/49
|
20
20
|
stmt = sqlalchemy.select(sb_Owners.OwnerID).where(
|
21
21
|
sb_Owners.Owner == user.preferred_username,
|
@@ -36,7 +36,7 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
36
36
|
|
37
37
|
@staticmethod
|
38
38
|
def get_pfn(bucket_name: str, user: UserInfo, sandbox_info: SandboxInfo) -> str:
|
39
|
-
"""Get the sandbox's user namespaced and content addressed PFN"""
|
39
|
+
"""Get the sandbox's user namespaced and content addressed PFN."""
|
40
40
|
parts = [
|
41
41
|
"S3",
|
42
42
|
bucket_name,
|
@@ -50,7 +50,7 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
50
50
|
async def insert_sandbox(
|
51
51
|
self, se_name: str, user: UserInfo, pfn: str, size: int
|
52
52
|
) -> None:
|
53
|
-
"""Add a new sandbox in SandboxMetadataDB"""
|
53
|
+
"""Add a new sandbox in SandboxMetadataDB."""
|
54
54
|
# TODO: Follow https://github.com/DIRACGrid/diracx/issues/49
|
55
55
|
owner_id = await self.upsert_owner(user)
|
56
56
|
stmt = sqlalchemy.insert(sb_SandBoxes).values(
|
@@ -88,13 +88,13 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
88
88
|
|
89
89
|
@staticmethod
|
90
90
|
def jobid_to_entity_id(job_id: int) -> str:
|
91
|
-
"""Define the entity id as 'Entity:entity_id' due to the DB definition"""
|
91
|
+
"""Define the entity id as 'Entity:entity_id' due to the DB definition."""
|
92
92
|
return f"Job:{job_id}"
|
93
93
|
|
94
94
|
async def get_sandbox_assigned_to_job(
|
95
95
|
self, job_id: int, sb_type: SandboxType
|
96
96
|
) -> list[Any]:
|
97
|
-
"""Get the sandbox assign to job"""
|
97
|
+
"""Get the sandbox assign to job."""
|
98
98
|
entity_id = self.jobid_to_entity_id(job_id)
|
99
99
|
stmt = (
|
100
100
|
sqlalchemy.select(sb_SandBoxes.SEPFN)
|
@@ -114,7 +114,7 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
114
114
|
sb_type: SandboxType,
|
115
115
|
se_name: str,
|
116
116
|
) -> None:
|
117
|
-
"""Mapp sandbox and jobs"""
|
117
|
+
"""Mapp sandbox and jobs."""
|
118
118
|
for job_id in jobs_ids:
|
119
119
|
# Define the entity id as 'Entity:entity_id' due to the DB definition:
|
120
120
|
entity_id = self.jobid_to_entity_id(job_id)
|
@@ -140,12 +140,14 @@ class SandboxMetadataDB(BaseSQLDB):
|
|
140
140
|
assert result.rowcount == 1
|
141
141
|
|
142
142
|
async def unassign_sandboxes_to_jobs(self, jobs_ids: list[int]) -> None:
|
143
|
-
"""Delete mapping between jobs and sandboxes"""
|
143
|
+
"""Delete mapping between jobs and sandboxes."""
|
144
144
|
for job_id in jobs_ids:
|
145
145
|
entity_id = self.jobid_to_entity_id(job_id)
|
146
|
-
sb_sel_stmt = sqlalchemy.select(
|
147
|
-
|
148
|
-
|
146
|
+
sb_sel_stmt = sqlalchemy.select(sb_SandBoxes.SBId)
|
147
|
+
sb_sel_stmt = sb_sel_stmt.join(
|
148
|
+
sb_EntityMapping, sb_EntityMapping.SBId == sb_SandBoxes.SBId
|
149
|
+
)
|
150
|
+
sb_sel_stmt = sb_sel_stmt.where(sb_EntityMapping.EntityId == entity_id)
|
149
151
|
|
150
152
|
result = await self.conn.execute(sb_sel_stmt)
|
151
153
|
sb_ids = [row.SBId for row in result]
|
File without changes
|
@@ -0,0 +1,261 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
from sqlalchemy import delete, func, select, update
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
pass
|
9
|
+
|
10
|
+
from diracx.core.properties import JOB_SHARING, SecurityProperty
|
11
|
+
|
12
|
+
from ..utils import BaseSQLDB
|
13
|
+
from .schema import (
|
14
|
+
BannedSitesQueue,
|
15
|
+
GridCEsQueue,
|
16
|
+
JobsQueue,
|
17
|
+
JobTypesQueue,
|
18
|
+
PlatformsQueue,
|
19
|
+
SitesQueue,
|
20
|
+
TagsQueue,
|
21
|
+
TaskQueueDBBase,
|
22
|
+
TaskQueues,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
class TaskQueueDB(BaseSQLDB):
|
27
|
+
metadata = TaskQueueDBBase.metadata
|
28
|
+
|
29
|
+
async def get_tq_infos_for_jobs(
|
30
|
+
self, job_ids: list[int]
|
31
|
+
) -> set[tuple[int, str, str, str]]:
|
32
|
+
"""Get the task queue info for given jobs."""
|
33
|
+
stmt = (
|
34
|
+
select(
|
35
|
+
TaskQueues.TQId, TaskQueues.Owner, TaskQueues.OwnerGroup, TaskQueues.VO
|
36
|
+
)
|
37
|
+
.join(JobsQueue, TaskQueues.TQId == JobsQueue.TQId)
|
38
|
+
.where(JobsQueue.JobId.in_(job_ids))
|
39
|
+
)
|
40
|
+
return set(
|
41
|
+
(int(row[0]), str(row[1]), str(row[2]), str(row[3]))
|
42
|
+
for row in (await self.conn.execute(stmt)).all()
|
43
|
+
)
|
44
|
+
|
45
|
+
async def get_owner_for_task_queue(self, tq_id: int) -> dict[str, str]:
|
46
|
+
"""Get the owner and owner group for a task queue."""
|
47
|
+
stmt = select(TaskQueues.Owner, TaskQueues.OwnerGroup, TaskQueues.VO).where(
|
48
|
+
TaskQueues.TQId == tq_id
|
49
|
+
)
|
50
|
+
return dict((await self.conn.execute(stmt)).one()._mapping)
|
51
|
+
|
52
|
+
async def remove_job(self, job_id: int):
|
53
|
+
"""Remove a job from the task queues."""
|
54
|
+
stmt = delete(JobsQueue).where(JobsQueue.JobId == job_id)
|
55
|
+
await self.conn.execute(stmt)
|
56
|
+
|
57
|
+
async def remove_jobs(self, job_ids: list[int]):
|
58
|
+
"""Remove jobs from the task queues."""
|
59
|
+
stmt = delete(JobsQueue).where(JobsQueue.JobId.in_(job_ids))
|
60
|
+
await self.conn.execute(stmt)
|
61
|
+
|
62
|
+
async def delete_task_queue_if_empty(
|
63
|
+
self,
|
64
|
+
tq_id: int,
|
65
|
+
tq_owner: str,
|
66
|
+
tq_group: str,
|
67
|
+
job_share: int,
|
68
|
+
group_properties: set[SecurityProperty],
|
69
|
+
enable_shares_correction: bool,
|
70
|
+
allow_background_tqs: bool,
|
71
|
+
):
|
72
|
+
"""Try to delete a task queue if it's empty."""
|
73
|
+
# Check if the task queue is empty
|
74
|
+
stmt = (
|
75
|
+
select(TaskQueues.TQId)
|
76
|
+
.where(TaskQueues.Enabled >= 1)
|
77
|
+
.where(TaskQueues.TQId == tq_id)
|
78
|
+
.where(~TaskQueues.TQId.in_(select(JobsQueue.TQId)))
|
79
|
+
)
|
80
|
+
rows = await self.conn.execute(stmt)
|
81
|
+
if not rows.rowcount:
|
82
|
+
return
|
83
|
+
|
84
|
+
# Deleting the task queue (the other tables will be deleted in cascade)
|
85
|
+
stmt = delete(TaskQueues).where(TaskQueues.TQId == tq_id)
|
86
|
+
await self.conn.execute(stmt)
|
87
|
+
|
88
|
+
await self.recalculate_tq_shares_for_entity(
|
89
|
+
tq_owner,
|
90
|
+
tq_group,
|
91
|
+
job_share,
|
92
|
+
group_properties,
|
93
|
+
enable_shares_correction,
|
94
|
+
allow_background_tqs,
|
95
|
+
)
|
96
|
+
|
97
|
+
async def recalculate_tq_shares_for_entity(
|
98
|
+
self,
|
99
|
+
owner: str,
|
100
|
+
group: str,
|
101
|
+
job_share: int,
|
102
|
+
group_properties: set[SecurityProperty],
|
103
|
+
enable_shares_correction: bool,
|
104
|
+
allow_background_tqs: bool,
|
105
|
+
):
|
106
|
+
"""Recalculate the shares for a user/userGroup combo."""
|
107
|
+
if JOB_SHARING in group_properties:
|
108
|
+
# If group has JobSharing just set prio for that entry, user is irrelevant
|
109
|
+
return await self.__set_priorities_for_entity(
|
110
|
+
owner, group, job_share, group_properties, allow_background_tqs
|
111
|
+
)
|
112
|
+
|
113
|
+
stmt = (
|
114
|
+
select(TaskQueues.Owner, func.count(TaskQueues.Owner))
|
115
|
+
.where(TaskQueues.OwnerGroup == group)
|
116
|
+
.group_by(TaskQueues.Owner)
|
117
|
+
)
|
118
|
+
rows = await self.conn.execute(stmt)
|
119
|
+
# make the rows a list of tuples
|
120
|
+
# Get owners in this group and the amount of times they appear
|
121
|
+
# TODO: I guess the rows are already a list of tupes
|
122
|
+
# maybe refactor
|
123
|
+
data = [(r[0], r[1]) for r in rows if r]
|
124
|
+
numOwners = len(data)
|
125
|
+
# If there are no owners do now
|
126
|
+
if numOwners == 0:
|
127
|
+
return
|
128
|
+
# Split the share amongst the number of owners
|
129
|
+
entities_shares = {row[0]: job_share / numOwners for row in data}
|
130
|
+
|
131
|
+
# TODO: implement the following
|
132
|
+
# If corrector is enabled let it work it's magic
|
133
|
+
# if enable_shares_correction:
|
134
|
+
# entities_shares = await self.__shares_corrector.correct_shares(
|
135
|
+
# entitiesShares, group=group
|
136
|
+
# )
|
137
|
+
|
138
|
+
# Keep updating
|
139
|
+
owners = dict(data)
|
140
|
+
# IF the user is already known and has more than 1 tq, the rest of the users don't need to be modified
|
141
|
+
# (The number of owners didn't change)
|
142
|
+
if owner in owners and owners[owner] > 1:
|
143
|
+
await self.__set_priorities_for_entity(
|
144
|
+
owner,
|
145
|
+
group,
|
146
|
+
entities_shares[owner],
|
147
|
+
group_properties,
|
148
|
+
allow_background_tqs,
|
149
|
+
)
|
150
|
+
return
|
151
|
+
# Oops the number of owners may have changed so we recalculate the prio for all owners in the group
|
152
|
+
for owner in owners:
|
153
|
+
await self.__set_priorities_for_entity(
|
154
|
+
owner,
|
155
|
+
group,
|
156
|
+
entities_shares[owner],
|
157
|
+
group_properties,
|
158
|
+
allow_background_tqs,
|
159
|
+
)
|
160
|
+
|
161
|
+
async def __set_priorities_for_entity(
|
162
|
+
self,
|
163
|
+
owner: str,
|
164
|
+
group: str,
|
165
|
+
share,
|
166
|
+
properties: set[SecurityProperty],
|
167
|
+
allow_background_tqs: bool,
|
168
|
+
):
|
169
|
+
"""Set the priority for a user/userGroup combo given a splitted share."""
|
170
|
+
from DIRAC.WorkloadManagementSystem.DB.TaskQueueDB import calculate_priority
|
171
|
+
|
172
|
+
stmt = (
|
173
|
+
select(
|
174
|
+
TaskQueues.TQId,
|
175
|
+
func.sum(JobsQueue.RealPriority) / func.count(JobsQueue.RealPriority),
|
176
|
+
)
|
177
|
+
.join(JobsQueue, TaskQueues.TQId == JobsQueue.TQId)
|
178
|
+
.where(TaskQueues.OwnerGroup == group)
|
179
|
+
.group_by(TaskQueues.TQId)
|
180
|
+
)
|
181
|
+
if JOB_SHARING not in properties:
|
182
|
+
stmt = stmt.where(TaskQueues.Owner == owner)
|
183
|
+
rows = await self.conn.execute(stmt)
|
184
|
+
tq_dict: dict[int, float] = {tq_id: priority for tq_id, priority in rows}
|
185
|
+
|
186
|
+
if not tq_dict:
|
187
|
+
return
|
188
|
+
|
189
|
+
rows = await self.retrieve_task_queues(list(tq_dict))
|
190
|
+
|
191
|
+
prio_dict = calculate_priority(tq_dict, rows, share, allow_background_tqs)
|
192
|
+
|
193
|
+
# Execute updates
|
194
|
+
for prio, tqs in prio_dict.items():
|
195
|
+
update_stmt = (
|
196
|
+
update(TaskQueues).where(TaskQueues.TQId.in_(tqs)).values(Priority=prio)
|
197
|
+
)
|
198
|
+
await self.conn.execute(update_stmt)
|
199
|
+
|
200
|
+
async def retrieve_task_queues(self, tq_id_list=None):
|
201
|
+
"""Get all the task queues."""
|
202
|
+
if tq_id_list is not None and not tq_id_list:
|
203
|
+
# Empty list => Fast-track no matches
|
204
|
+
return {}
|
205
|
+
|
206
|
+
stmt = (
|
207
|
+
select(
|
208
|
+
TaskQueues.TQId,
|
209
|
+
TaskQueues.Priority,
|
210
|
+
func.count(JobsQueue.TQId).label("Jobs"),
|
211
|
+
TaskQueues.Owner,
|
212
|
+
TaskQueues.OwnerGroup,
|
213
|
+
TaskQueues.VO,
|
214
|
+
TaskQueues.CPUTime,
|
215
|
+
)
|
216
|
+
.join(JobsQueue, TaskQueues.TQId == JobsQueue.TQId)
|
217
|
+
.join(SitesQueue, TaskQueues.TQId == SitesQueue.TQId)
|
218
|
+
.join(GridCEsQueue, TaskQueues.TQId == GridCEsQueue.TQId)
|
219
|
+
.group_by(
|
220
|
+
TaskQueues.TQId,
|
221
|
+
TaskQueues.Priority,
|
222
|
+
TaskQueues.Owner,
|
223
|
+
TaskQueues.OwnerGroup,
|
224
|
+
TaskQueues.VO,
|
225
|
+
TaskQueues.CPUTime,
|
226
|
+
)
|
227
|
+
)
|
228
|
+
if tq_id_list is not None:
|
229
|
+
stmt = stmt.where(TaskQueues.TQId.in_(tq_id_list))
|
230
|
+
|
231
|
+
tq_data: dict[int, dict[str, list[str]]] = dict(
|
232
|
+
dict(row._mapping) for row in await self.conn.execute(stmt)
|
233
|
+
)
|
234
|
+
# TODO: the line above should be equivalent to the following commented code, check this is the case
|
235
|
+
# for record in rows:
|
236
|
+
# tqId = record[0]
|
237
|
+
# tqData[tqId] = {
|
238
|
+
# "Priority": record[1],
|
239
|
+
# "Jobs": record[2],
|
240
|
+
# "Owner": record[3],
|
241
|
+
# "OwnerGroup": record[4],
|
242
|
+
# "VO": record[5],
|
243
|
+
# "CPUTime": record[6],
|
244
|
+
# }
|
245
|
+
|
246
|
+
for tq_id in tq_data:
|
247
|
+
# TODO: maybe factorize this handy tuple list
|
248
|
+
for table, field in {
|
249
|
+
(SitesQueue, "Sites"),
|
250
|
+
(GridCEsQueue, "GridCEs"),
|
251
|
+
(BannedSitesQueue, "BannedSites"),
|
252
|
+
(PlatformsQueue, "Platforms"),
|
253
|
+
(JobTypesQueue, "JobTypes"),
|
254
|
+
(TagsQueue, "Tags"),
|
255
|
+
}:
|
256
|
+
stmt = select(table.Value).where(table.TQId == tq_id)
|
257
|
+
tq_data[tq_id][field] = list(
|
258
|
+
row[0] for row in await self.conn.execute(stmt)
|
259
|
+
)
|
260
|
+
|
261
|
+
return tq_data
|
@@ -0,0 +1,109 @@
|
|
1
|
+
from sqlalchemy import (
|
2
|
+
BigInteger,
|
3
|
+
Boolean,
|
4
|
+
Float,
|
5
|
+
ForeignKey,
|
6
|
+
Index,
|
7
|
+
Integer,
|
8
|
+
String,
|
9
|
+
)
|
10
|
+
from sqlalchemy.orm import declarative_base
|
11
|
+
|
12
|
+
from ..utils import Column
|
13
|
+
|
14
|
+
TaskQueueDBBase = declarative_base()
|
15
|
+
|
16
|
+
|
17
|
+
class TaskQueues(TaskQueueDBBase):
|
18
|
+
__tablename__ = "tq_TaskQueues"
|
19
|
+
TQId = Column(Integer, primary_key=True)
|
20
|
+
Owner = Column(String(255), nullable=False)
|
21
|
+
OwnerGroup = Column(String(32), nullable=False)
|
22
|
+
VO = Column(String(32), nullable=False)
|
23
|
+
CPUTime = Column(BigInteger, nullable=False)
|
24
|
+
Priority = Column(Float, nullable=False)
|
25
|
+
Enabled = Column(Boolean, nullable=False, default=0)
|
26
|
+
__table_args__ = (Index("TQOwner", "Owner", "OwnerGroup", "CPUTime"),)
|
27
|
+
|
28
|
+
|
29
|
+
class JobsQueue(TaskQueueDBBase):
|
30
|
+
__tablename__ = "tq_Jobs"
|
31
|
+
TQId = Column(
|
32
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
33
|
+
)
|
34
|
+
JobId = Column(Integer, primary_key=True)
|
35
|
+
Priority = Column(Integer, nullable=False)
|
36
|
+
RealPriority = Column(Float, nullable=False)
|
37
|
+
__table_args__ = (Index("TaskIndex", "TQId"),)
|
38
|
+
|
39
|
+
|
40
|
+
class SitesQueue(TaskQueueDBBase):
|
41
|
+
__tablename__ = "tq_TQToSites"
|
42
|
+
TQId = Column(
|
43
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
44
|
+
)
|
45
|
+
Value = Column(String(64), primary_key=True)
|
46
|
+
__table_args__ = (
|
47
|
+
Index("SitesTaskIndex", "TQId"),
|
48
|
+
Index("SitesIndex", "Value"),
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
class GridCEsQueue(TaskQueueDBBase):
|
53
|
+
__tablename__ = "tq_TQToGridCEs"
|
54
|
+
TQId = Column(
|
55
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
56
|
+
)
|
57
|
+
Value = Column(String(64), primary_key=True)
|
58
|
+
__table_args__ = (
|
59
|
+
Index("GridCEsTaskIndex", "TQId"),
|
60
|
+
Index("GridCEsValueIndex", "Value"),
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
class BannedSitesQueue(TaskQueueDBBase):
|
65
|
+
__tablename__ = "tq_TQToBannedSites"
|
66
|
+
TQId = Column(
|
67
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
68
|
+
)
|
69
|
+
Value = Column(String(64), primary_key=True)
|
70
|
+
__table_args__ = (
|
71
|
+
Index("BannedSitesTaskIndex", "TQId"),
|
72
|
+
Index("BannedSitesValueIndex", "Value"),
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
class PlatformsQueue(TaskQueueDBBase):
|
77
|
+
__tablename__ = "tq_TQToPlatforms"
|
78
|
+
TQId = Column(
|
79
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
80
|
+
)
|
81
|
+
Value = Column(String(64), primary_key=True)
|
82
|
+
__table_args__ = (
|
83
|
+
Index("PlatformsTaskIndex", "TQId"),
|
84
|
+
Index("PlatformsValueIndex", "Value"),
|
85
|
+
)
|
86
|
+
|
87
|
+
|
88
|
+
class JobTypesQueue(TaskQueueDBBase):
|
89
|
+
__tablename__ = "tq_TQToJobTypes"
|
90
|
+
TQId = Column(
|
91
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
92
|
+
)
|
93
|
+
Value = Column(String(64), primary_key=True)
|
94
|
+
__table_args__ = (
|
95
|
+
Index("JobTypesTaskIndex", "TQId"),
|
96
|
+
Index("JobTypesValueIndex", "Value"),
|
97
|
+
)
|
98
|
+
|
99
|
+
|
100
|
+
class TagsQueue(TaskQueueDBBase):
|
101
|
+
__tablename__ = "tq_TQToTags"
|
102
|
+
TQId = Column(
|
103
|
+
Integer, ForeignKey("tq_TaskQueues.TQId", ondelete="CASCADE"), primary_key=True
|
104
|
+
)
|
105
|
+
Value = Column(String(64), primary_key=True)
|
106
|
+
__table_args__ = (
|
107
|
+
Index("TagsTaskIndex", "TQId"),
|
108
|
+
Index("TagsValueIndex", "Value"),
|
109
|
+
)
|